From 9e2c1feae10b353cbb8bcb49a7bb5b643fe2c212 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 16 Jan 2026 21:52:59 +0800 Subject: [PATCH 01/53] =?UTF-8?q?feat=EF=BC=9A=E3=80=90iot=E3=80=91modbus-?= =?UTF-8?q?tcp=20=E5=8D=8F=E8=AE=AE=E6=8E=A5=E5=85=A5=EF=BC=9A10%=20?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=EF=BC=9A=E5=9F=BA=E4=BA=8E=20cheeky?= =?UTF-8?q?-plotting-pumpkin.md=20=E8=A7=84=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/api/device/IoTDeviceApiImpl.java | 58 +++- .../IotDeviceModbusConfigController.java | 84 ++++++ .../IotDeviceModbusPointController.java | 84 ++++++ .../IotDeviceModbusConfigPageReqVO.java | 25 ++ .../modbus/IotDeviceModbusConfigRespVO.java | 42 +++ .../IotDeviceModbusConfigSaveReqVO.java | 41 +++ .../modbus/IotDeviceModbusPointPageReqVO.java | 30 ++ .../vo/modbus/IotDeviceModbusPointRespVO.java | 55 ++++ .../modbus/IotDeviceModbusPointSaveReqVO.java | 54 ++++ .../device/IotDeviceModbusConfigDO.java | 62 ++++ .../device/IotDeviceModbusPointDO.java | 103 +++++++ .../device/IotDeviceModbusConfigMapper.java | 36 +++ .../device/IotDeviceModbusPointMapper.java | 53 ++++ .../module/iot/enums/ErrorCodeConstants.java | 8 + .../enums/device/IotModbusByteOrderEnum.java | 61 ++++ .../device/IotModbusFunctionCodeEnum.java | 80 +++++ .../device/IotModbusRawDataTypeEnum.java | 64 ++++ .../device/IotDeviceModbusConfigService.java | 71 +++++ .../IotDeviceModbusConfigServiceImpl.java | 122 ++++++++ .../device/IotDeviceModbusPointService.java | 72 +++++ .../IotDeviceModbusPointServiceImpl.java | 138 +++++++++ .../iot/core/biz/IotDeviceCommonApi.java | 10 + .../biz/dto/IotModbusDeviceConfigRespDTO.java | 62 ++++ .../core/biz/dto/IotModbusPointRespDTO.java | 74 +++++ .../yudao-module-iot-gateway/pom.xml | 9 +- .../config/IotGatewayConfiguration.java | 35 +++ .../gateway/config/IotGatewayProperties.java | 22 ++ .../modbustcp/IotModbusDataConverter.java | 281 ++++++++++++++++++ .../modbustcp/IotModbusTcpClient.java | 195 ++++++++++++ .../IotModbusTcpConfigCacheService.java | 106 +++++++ .../IotModbusTcpConnectionManager.java | 255 ++++++++++++++++ .../IotModbusTcpDownstreamHandler.java | 128 ++++++++ .../IotModbusTcpDownstreamSubscriber.java | 55 ++++ .../modbustcp/IotModbusTcpPollScheduler.java | 134 +++++++++ .../IotModbusTcpUpstreamHandler.java | 78 +++++ .../IotModbusTcpUpstreamProtocol.java | 128 ++++++++ .../device/remote/IotDeviceApiImpl.java | 23 +- .../src/main/resources/application.yaml | 3 + 38 files changed, 2933 insertions(+), 8 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigPageReqVO.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java index eb55b1852a..5a01518995 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java @@ -4,11 +4,13 @@ import cn.iocoder.yudao.framework.common.enums.RpcConstants; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.*; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import jakarta.annotation.Resource; @@ -19,6 +21,9 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import java.util.ArrayList; +import java.util.List; + import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** @@ -35,6 +40,10 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { private IotDeviceService deviceService; @Resource private IotProductService productService; + @Resource + private IotDeviceModbusConfigService modbusConfigService; + @Resource + private IotDeviceModbusPointService modbusPointService; @Override @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth") @@ -57,4 +66,47 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { })); } + @Override + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/enabled-configs") + @PermitAll + public CommonResult> getEnabledModbusDeviceConfigs() { + // 1. 获取所有启用的 Modbus 连接配置 + List configList = modbusConfigService.getEnabledModbusConfigList(); + if (configList.isEmpty()) { + return success(new ArrayList<>()); + } + + // 2. 组装返回结果 + List result = new ArrayList<>(configList.size()); + for (IotDeviceModbusConfigDO config : configList) { + // 2.1 获取设备信息 + // TODO @AI:设备需要批量读取;(先暂时不处理) + IotDeviceDO device = deviceService.getDeviceFromCache(config.getDeviceId()); + if (device == null) { + continue; + } + + // 2.2 获取启用的点位列表 + // TODO @AI:看看是不是批量读取; + List pointList = modbusPointService.getEnabledModbusPointListByDeviceId(config.getDeviceId()); + + // 2.3 构建 DTO + IotModbusDeviceConfigRespDTO dto = new IotModbusDeviceConfigRespDTO(); + dto.setDeviceId(config.getDeviceId()); + // TODO @AI:这个 productKey、deviceName 这个字段,要不要冗余到 IotDeviceModbusConfigDO 里面?(先暂时不处理) + dto.setProductKey(device.getProductKey()); + dto.setDeviceName(device.getDeviceName()); + dto.setTenantId(device.getTenantId()); + // TODO @AI:看看 dto 的转换,能不能通过 beanutils copy + dto.setIp(config.getIp()); + dto.setPort(config.getPort()); + dto.setSlaveId(config.getSlaveId()); + dto.setTimeout(config.getTimeout()); + dto.setRetryInterval(config.getRetryInterval()); + dto.setPoints(BeanUtils.toBean(pointList, IotModbusPointRespDTO.class)); + result.add(dto); + } + return success(result); + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java new file mode 100644 index 0000000000..14648ed9a5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 设备 Modbus 连接配置") +@RestController +@RequestMapping("/iot/device-modbus-config") +@Validated +public class IotDeviceModbusConfigController { + + @Resource + private IotDeviceModbusConfigService modbusConfigService; + + // TODO @AI:create 和 update 合并成 save 接口; + @PostMapping("/create") + @Operation(summary = "创建设备 Modbus 连接配置") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:create')") + public CommonResult createModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO createReqVO) { + return success(modbusConfigService.createModbusConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新设备 Modbus 连接配置") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:update')") + public CommonResult updateModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO updateReqVO) { + modbusConfigService.updateModbusConfig(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除设备 Modbus 连接配置") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:delete')") + public CommonResult deleteModbusConfig(@RequestParam("id") Long id) { + modbusConfigService.deleteModbusConfig(id); + return success(true); + } + + // TODO @AI:这个接口改造,支持 id 或者 deviceId;二选一查询; + @GetMapping("/get") + @Operation(summary = "获得设备 Modbus 连接配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") + public CommonResult getModbusConfig(@RequestParam("id") Long id) { + IotDeviceModbusConfigDO modbusConfig = modbusConfigService.getModbusConfig(id); + return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class)); + } + + // TODO @AI:合并到 getModbusConfig 接口里; + @GetMapping("/get-by-device-id") + @Operation(summary = "根据设备编号获得 Modbus 连接配置") + @Parameter(name = "deviceId", description = "设备编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") + public CommonResult getModbusConfigByDeviceId(@RequestParam("deviceId") Long deviceId) { + IotDeviceModbusConfigDO modbusConfig = modbusConfigService.getModbusConfigByDeviceId(deviceId); + return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得设备 Modbus 连接配置分页") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") + public CommonResult> getModbusConfigPage(@Valid IotDeviceModbusConfigPageReqVO pageReqVO) { + PageResult pageResult = modbusConfigService.getModbusConfigPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDeviceModbusConfigRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java new file mode 100644 index 0000000000..55acc24133 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 设备 Modbus 点位配置") +@RestController +@RequestMapping("/iot/device-modbus-point") +@Validated +public class IotDeviceModbusPointController { + + @Resource + private IotDeviceModbusPointService modbusPointService; + + @PostMapping("/create") + @Operation(summary = "创建设备 Modbus 点位配置") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:create')") + public CommonResult createModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO createReqVO) { + return success(modbusPointService.createModbusPoint(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新设备 Modbus 点位配置") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:update')") + public CommonResult updateModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO updateReqVO) { + modbusPointService.updateModbusPoint(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除设备 Modbus 点位配置") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:delete')") + public CommonResult deleteModbusPoint(@RequestParam("id") Long id) { + modbusPointService.deleteModbusPoint(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得设备 Modbus 点位配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") + public CommonResult getModbusPoint(@RequestParam("id") Long id) { + IotDeviceModbusPointDO modbusPoint = modbusPointService.getModbusPoint(id); + return success(BeanUtils.toBean(modbusPoint, IotDeviceModbusPointRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得设备 Modbus 点位配置分页") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") + public CommonResult> getModbusPointPage(@Valid IotDeviceModbusPointPageReqVO pageReqVO) { + PageResult pageResult = modbusPointService.getModbusPointPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDeviceModbusPointRespVO.class)); + } + + // TODO @AI:应该用不上这个接口?只需要 getModbusPointPage 分页 + @GetMapping("/list-by-device-id") + @Operation(summary = "根据设备编号获得 Modbus 点位配置列表") + @Parameter(name = "deviceId", description = "设备编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") + public CommonResult> getModbusPointListByDeviceId(@RequestParam("deviceId") Long deviceId) { + List list = modbusPointService.getModbusPointListByDeviceId(deviceId); + return success(BeanUtils.toBean(list, IotDeviceModbusPointRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigPageReqVO.java new file mode 100644 index 0000000000..61ff79d74a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigPageReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +// TODO @AI:不需要分页接口; +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IotDeviceModbusConfigPageReqVO extends PageParam { + + @Schema(description = "设备编号", example = "1024") + private Long deviceId; + + @Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100") + private String ip; + + @Schema(description = "状态", example = "0") + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java new file mode 100644 index 0000000000..304b37ff16 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置 Response VO") +@Data +public class IotDeviceModbusConfigRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long deviceId; + + @Schema(description = "设备名称", example = "温湿度传感器") + private String deviceName; + + @Schema(description = "Modbus 服务器 IP 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100") + private String ip; + + @Schema(description = "Modbus 端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "502") + private Integer port; + + @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer slaveId; + + @Schema(description = "连接超时时间(毫秒)", example = "3000") + private Integer timeout; + + @Schema(description = "重试间隔(毫秒)", example = "1000") + private Integer retryInterval; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java new file mode 100644 index 0000000000..155ce5fa8d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置新增/修改 Request VO") +@Data +public class IotDeviceModbusConfigSaveReqVO { + + @Schema(description = "主键", example = "1") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "设备编号不能为空") + private Long deviceId; + + @Schema(description = "Modbus 服务器 IP 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100") + @NotEmpty(message = "Modbus 服务器 IP 地址不能为空") + private String ip; + + @Schema(description = "Modbus 端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "502") + @NotNull(message = "Modbus 端口不能为空") + private Integer port; + + @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "从站地址不能为空") + private Integer slaveId; + + @Schema(description = "连接超时时间(毫秒)", example = "3000") + private Integer timeout; + + @Schema(description = "重试间隔(毫秒)", example = "1000") + private Integer retryInterval; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java new file mode 100644 index 0000000000..344e8ce121 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IotDeviceModbusPointPageReqVO extends PageParam { + + @Schema(description = "设备编号", example = "1024") + private Long deviceId; + + @Schema(description = "属性标识符", example = "temperature") + private String identifier; + + @Schema(description = "属性名称", example = "温度") + private String name; + + @Schema(description = "Modbus 功能码", example = "3") + private Integer functionCode; + + @Schema(description = "状态", example = "0") + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java new file mode 100644 index 0000000000..c590e3e3f3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置 Response VO") +@Data +public class IotDeviceModbusPointRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long deviceId; + + @Schema(description = "物模型属性编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long thingModelId; + + @Schema(description = "属性标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature") + private String identifier; + + @Schema(description = "属性名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "温度") + private String name; + + @Schema(description = "Modbus 功能码", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer functionCode; + + @Schema(description = "寄存器起始地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer registerAddress; + + @Schema(description = "寄存器数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer registerCount; + + @Schema(description = "字节序", requiredMode = Schema.RequiredMode.REQUIRED, example = "AB") + private String byteOrder; + + @Schema(description = "原始数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "INT16") + private String rawDataType; + + @Schema(description = "缩放因子", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0") + private BigDecimal scale; + + @Schema(description = "轮询间隔(毫秒)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5000") + private Integer pollInterval; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java new file mode 100644 index 0000000000..18aea2bf61 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置新增/修改 Request VO") +@Data +public class IotDeviceModbusPointSaveReqVO { + + @Schema(description = "主键", example = "1") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "设备编号不能为空") + private Long deviceId; + + @Schema(description = "物模型属性编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "物模型属性编号不能为空") + private Long thingModelId; + + @Schema(description = "Modbus 功能码", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "Modbus 功能码不能为空") + private Integer functionCode; + + @Schema(description = "寄存器起始地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "寄存器起始地址不能为空") + private Integer registerAddress; + + @Schema(description = "寄存器数量", example = "1") + private Integer registerCount; + + @Schema(description = "字节序", requiredMode = Schema.RequiredMode.REQUIRED, example = "AB") + @NotEmpty(message = "字节序不能为空") + private String byteOrder; + + @Schema(description = "原始数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "INT16") + @NotEmpty(message = "原始数据类型不能为空") + private String rawDataType; + + @Schema(description = "缩放因子", example = "1.0") + private BigDecimal scale; + + @Schema(description = "轮询间隔(毫秒)", example = "5000") + private Integer pollInterval; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java new file mode 100644 index 0000000000..627ec1b336 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * IoT 设备 Modbus 连接配置 DO + * + * @author 芋道源码 + */ +@TableName("iot_device_modbus_config") +@KeySequence("iot_device_modbus_config_seq") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceModbusConfigDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + // TODO @AI:增加 productId; + /** + * 设备编号 + * + * 关联 {@link IotDeviceDO#getId()} + */ + private Long deviceId; + + /** + * Modbus 服务器 IP 地址 + */ + private String ip; + /** + * Modbus 服务器端口 + */ + private Integer port; + /** + * 从站地址 + */ + private Integer slaveId; + /** + * 连接超时时间,单位:毫秒 + */ + private Integer timeout; + /** + * 重试间隔,单位:毫秒 + */ + private Integer retryInterval; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java new file mode 100644 index 0000000000..dc0466b589 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.enums.device.IotModbusByteOrderEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotModbusFunctionCodeEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotModbusRawDataTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; + +/** + * IoT 设备 Modbus 点位配置 DO + * + * @author 芋道源码 + */ +@TableName("iot_device_modbus_point") +@KeySequence("iot_device_modbus_point_seq") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceModbusPointDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + // TODO @AI:增加 productId; + /** + * 设备编号 + * + * 关联 {@link IotDeviceDO#getId()} + */ + private Long deviceId; + /** + * 物模型属性编号 + * + * 关联 {@link IotThingModelDO#getId()} + */ + private Long thingModelId; + // TODO @AI:每次物模型的变更时,需要按需刷下 identifier、name 配置; + /** + * 属性标识符 + * + * 冗余 {@link IotThingModelDO#getIdentifier()} + */ + private String identifier; + /** + * 属性名称 + * + * 冗余 {@link IotThingModelDO#getName()} + */ + private String name; + + // ========== Modbus 协议配置 ========== + + /** + * Modbus 功能码 + * + * 枚举 {@link IotModbusFunctionCodeEnum} + */ + private Integer functionCode; + /** + * 寄存器起始地址 + */ + private Integer registerAddress; + /** + * 寄存器数量 + */ + private Integer registerCount; + /** + * 字节序 + * + * 枚举 {@link IotModbusByteOrderEnum} + */ + private String byteOrder; + /** + * 原始数据类型 + * + * 枚举 {@link IotModbusRawDataTypeEnum} + */ + private String rawDataType; + /** + * 缩放因子 + */ + private BigDecimal scale; + /** + * 轮询间隔(毫秒) + */ + private Integer pollInterval; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java new file mode 100644 index 0000000000..c0053dd38f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * IoT 设备 Modbus 连接配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotDeviceModbusConfigMapper extends BaseMapperX { + + default PageResult selectPage(IotDeviceModbusConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotDeviceModbusConfigDO::getDeviceId, reqVO.getDeviceId()) + .likeIfPresent(IotDeviceModbusConfigDO::getIp, reqVO.getIp()) + .eqIfPresent(IotDeviceModbusConfigDO::getStatus, reqVO.getStatus()) + .orderByDesc(IotDeviceModbusConfigDO::getId)); + } + + default IotDeviceModbusConfigDO selectByDeviceId(Long deviceId) { + return selectOne(IotDeviceModbusConfigDO::getDeviceId, deviceId); + } + + default List selectListByStatus(Integer status) { + return selectList(IotDeviceModbusConfigDO::getStatus, status); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java new file mode 100644 index 0000000000..9043dd49f3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * IoT 设备 Modbus 点位配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotDeviceModbusPointMapper extends BaseMapperX { + + default PageResult selectPage(IotDeviceModbusPointPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotDeviceModbusPointDO::getDeviceId, reqVO.getDeviceId()) + .likeIfPresent(IotDeviceModbusPointDO::getIdentifier, reqVO.getIdentifier()) + .likeIfPresent(IotDeviceModbusPointDO::getName, reqVO.getName()) + .eqIfPresent(IotDeviceModbusPointDO::getFunctionCode, reqVO.getFunctionCode()) + .eqIfPresent(IotDeviceModbusPointDO::getStatus, reqVO.getStatus()) + .orderByDesc(IotDeviceModbusPointDO::getId)); + } + + default List selectListByDeviceId(Long deviceId) { + return selectList(IotDeviceModbusPointDO::getDeviceId, deviceId); + } + + // TODO @AI:是不是 selectList(f1, v1, f2, v2); + default List selectListByDeviceIdAndStatus(Long deviceId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(IotDeviceModbusPointDO::getDeviceId, deviceId) + .eq(IotDeviceModbusPointDO::getStatus, status)); + } + + // TODO @AI:是不是 selectOne(f1, v1, f2, v2); + default IotDeviceModbusPointDO selectByDeviceIdAndIdentifier(Long deviceId, String identifier) { + return selectOne(new LambdaQueryWrapperX() + .eq(IotDeviceModbusPointDO::getDeviceId, deviceId) + .eq(IotDeviceModbusPointDO::getIdentifier, identifier)); + } + + // TODO @AI:是不是删除这个方法; + default void deleteByDeviceId(Long deviceId) { + delete(IotDeviceModbusPointDO::getDeviceId, deviceId); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java index d1cf60e206..4ac4dd916d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java @@ -41,6 +41,14 @@ public interface ErrorCodeConstants { ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); + // ========== 设备 Modbus 配置 1-050-006-000 ========== + ErrorCode DEVICE_MODBUS_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "设备 Modbus 连接配置不存在"); + ErrorCode DEVICE_MODBUS_CONFIG_EXISTS = new ErrorCode(1_050_006_001, "设备 Modbus 连接配置已存在"); + + // ========== 设备 Modbus 点位 1-050-007-000 ========== + ErrorCode DEVICE_MODBUS_POINT_NOT_EXISTS = new ErrorCode(1_050_007_000, "设备 Modbus 点位配置不存在"); + ErrorCode DEVICE_MODBUS_POINT_EXISTS = new ErrorCode(1_050_007_001, "设备 Modbus 点位配置已存在"); + // ========== OTA 固件相关 1-050-008-000 ========== ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java new file mode 100644 index 0000000000..fdd0feb42e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT Modbus 字节序枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum IotModbusByteOrderEnum implements ArrayValuable { + + AB("AB", "大端序(16位)", 2), + BA("BA", "小端序(16位)", 2), + ABCD("ABCD", "大端序(32位)", 4), + CDAB("CDAB", "大端字交换(32位)", 4), + DCBA("DCBA", "小端序(32位)", 4), + BADC("BADC", "小端字交换(32位)", 4); + + public static final String[] ARRAYS = Arrays.stream(values()) + .map(IotModbusByteOrderEnum::getOrder) + .toArray(String[]::new); + + /** + * 字节序 + */ + private final String order; + /** + * 名称 + */ + private final String name; + /** + * 字节数 + */ + private final Integer byteCount; + + @Override + public String[] array() { + return ARRAYS; + } + + // TODO @AI:如果不需要,可以删除掉这个方法; + /** + * 根据字节序获取枚举 + * + * @param order 字节序 + * @return 枚举 + */ + public static IotModbusByteOrderEnum getByOrder(String order) { + return Arrays.stream(values()) + .filter(e -> e.getOrder().equals(order)) + .findFirst() + .orElse(null); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java new file mode 100644 index 0000000000..9fcc91e413 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT Modbus 功能码枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum IotModbusFunctionCodeEnum implements ArrayValuable { + + READ_COILS(1, "读线圈", true, 5, 15), + READ_DISCRETE_INPUTS(2, "读离散输入", false, null, null), + READ_HOLDING_REGISTERS(3, "读保持寄存器", true, 6, 16), + READ_INPUT_REGISTERS(4, "读输入寄存器", false, null, null); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(IotModbusFunctionCodeEnum::getCode) + .toArray(Integer[]::new); + + /** + * 功能码 + */ + private final Integer code; + /** + * 名称 + */ + private final String name; + /** + * 是否支持写操作 + */ + private final Boolean writable; + /** + * 单个写功能码 + */ + private final Integer writeSingleCode; + /** + * 多个写功能码 + */ + private final Integer writeMultipleCode; + + @Override + public Integer[] array() { + return ARRAYS; + } + + /** + * 根据功能码获取枚举 + * + * @param code 功能码 + * @return 枚举 + */ + public static IotModbusFunctionCodeEnum valueOf(Integer code) { + return Arrays.stream(values()) + .filter(e -> e.getCode().equals(code)) + .findFirst() + .orElse(null); + } + + // TODO @AI:如果用不到,可以暂时删除哈; + /** + * 获取写功能码 + * + * @param registerCount 寄存器数量 + * @return 写功能码 + */ + public Integer getWriteCode(int registerCount) { + if (!writable) { + return null; + } + return registerCount == 1 ? writeSingleCode : writeMultipleCode; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java new file mode 100644 index 0000000000..97efc14b50 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT Modbus 原始数据类型枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum IotModbusRawDataTypeEnum implements ArrayValuable { + + INT16("INT16", "有符号 16 位整数", 1), + UINT16("UINT16", "无符号 16 位整数", 1), + INT32("INT32", "有符号 32 位整数", 2), + UINT32("UINT32", "无符号 32 位整数", 2), + FLOAT("FLOAT", "32 位浮点数", 2), + DOUBLE("DOUBLE", "64 位浮点数", 4), + BOOLEAN("BOOLEAN", "布尔值(用于线圈)", 1), + STRING("STRING", "字符串", -1); // -1 表示可变长度 + + public static final String[] ARRAYS = Arrays.stream(values()) + .map(IotModbusRawDataTypeEnum::getType) + .toArray(String[]::new); + + /** + * 数据类型 + */ + private final String type; + /** + * 名称 + */ + private final String name; + // TODO @AI:去掉 default 会好点。null 表示可变; + /** + * 默认寄存器数量(-1 表示可变) + */ + private final Integer defaultRegisterCount; + + @Override + public String[] array() { + return ARRAYS; + } + + // TODO @AI:如果不用,可以删除掉 + /** + * 根据类型获取枚举 + * + * @param type 类型 + * @return 枚举 + */ + public static IotModbusRawDataTypeEnum getByType(String type) { + return Arrays.stream(values()) + .filter(e -> e.getType().equals(type)) + .findFirst() + .orElse(null); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java new file mode 100644 index 0000000000..ded475d73d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * IoT 设备 Modbus 连接配置 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceModbusConfigService { + + /** + * 创建设备 Modbus 连接配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO createReqVO); + + /** + * 更新设备 Modbus 连接配置 + * + * @param updateReqVO 更新信息 + */ + void updateModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO updateReqVO); + + /** + * 删除设备 Modbus 连接配置 + * + * @param id 编号 + */ + void deleteModbusConfig(Long id); + + /** + * 获得设备 Modbus 连接配置 + * + * @param id 编号 + * @return 设备 Modbus 连接配置 + */ + IotDeviceModbusConfigDO getModbusConfig(Long id); + + /** + * 根据设备编号获得 Modbus 连接配置 + * + * @param deviceId 设备编号 + * @return 设备 Modbus 连接配置 + */ + IotDeviceModbusConfigDO getModbusConfigByDeviceId(Long deviceId); + + /** + * 获得设备 Modbus 连接配置分页 + * + * @param pageReqVO 分页查询 + * @return 设备 Modbus 连接配置分页 + */ + PageResult getModbusConfigPage(IotDeviceModbusConfigPageReqVO pageReqVO); + + /** + * 获得所有启用的 Modbus 连接配置列表 + * + * @return 启用的 Modbus 连接配置列表 + */ + List getEnabledModbusConfigList(); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java new file mode 100644 index 0000000000..582258af98 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java @@ -0,0 +1,122 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusConfigMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODBUS_CONFIG_EXISTS; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODBUS_CONFIG_NOT_EXISTS; + +/** + * IoT 设备 Modbus 连接配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigService { + + @Resource + private IotDeviceModbusConfigMapper modbusConfigMapper; + + @Resource + private IotDeviceService deviceService; + + // TODO @AI:是不是搞成 save 接口?因为前端也不知道是 create 还是 update; + @Override + public Long createModbusConfig(IotDeviceModbusConfigSaveReqVO createReqVO) { + // 1.1 校验设备存在 + deviceService.validateDeviceExists(createReqVO.getDeviceId()); + // 1.2 校验设备是否已有 Modbus 配置 + validateModbusConfigUnique(createReqVO.getDeviceId(), null); + + // 2. 插入 + IotDeviceModbusConfigDO modbusConfig = BeanUtils.toBean(createReqVO, IotDeviceModbusConfigDO.class); + setDefaultValues(modbusConfig); + modbusConfigMapper.insert(modbusConfig); + return modbusConfig.getId(); + } + + @Override + public void updateModbusConfig(IotDeviceModbusConfigSaveReqVO updateReqVO) { + // 1.1 校验存在 + validateModbusConfigExists(updateReqVO.getId()); + // 1.2 校验设备存在 + deviceService.validateDeviceExists(updateReqVO.getDeviceId()); + // 1.3 校验唯一性 + validateModbusConfigUnique(updateReqVO.getDeviceId(), updateReqVO.getId()); + + // 2. 更新 + IotDeviceModbusConfigDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusConfigDO.class); + modbusConfigMapper.updateById(updateObj); + } + + @Override + public void deleteModbusConfig(Long id) { + // 校验存在 + validateModbusConfigExists(id); + // 删除 + modbusConfigMapper.deleteById(id); + } + + private void validateModbusConfigExists(Long id) { + if (modbusConfigMapper.selectById(id) == null) { + throw exception(DEVICE_MODBUS_CONFIG_NOT_EXISTS); + } + } + + private void validateModbusConfigUnique(Long deviceId, Long excludeId) { + IotDeviceModbusConfigDO config = modbusConfigMapper.selectByDeviceId(deviceId); + // TODO @AI:ObjUtil notequals + if (config != null && !config.getId().equals(excludeId)) { + throw exception(DEVICE_MODBUS_CONFIG_EXISTS); + } + } + + // TODO @AI:不要这个;前端都必须传递; + private void setDefaultValues(IotDeviceModbusConfigDO config) { + if (config.getPort() == null) { + config.setPort(502); + } + if (config.getSlaveId() == null) { + config.setSlaveId(1); + } + if (config.getTimeout() == null) { + config.setTimeout(3000); + } + if (config.getRetryInterval() == null) { + config.setRetryInterval(1000); + } + } + + @Override + public IotDeviceModbusConfigDO getModbusConfig(Long id) { + return modbusConfigMapper.selectById(id); + } + + @Override + public IotDeviceModbusConfigDO getModbusConfigByDeviceId(Long deviceId) { + return modbusConfigMapper.selectByDeviceId(deviceId); + } + + @Override + public PageResult getModbusConfigPage(IotDeviceModbusConfigPageReqVO pageReqVO) { + return modbusConfigMapper.selectPage(pageReqVO); + } + + @Override + public List getEnabledModbusConfigList() { + return modbusConfigMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java new file mode 100644 index 0000000000..daf2b0955f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * IoT 设备 Modbus 点位配置 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceModbusPointService { + + /** + * 创建设备 Modbus 点位配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createModbusPoint(@Valid IotDeviceModbusPointSaveReqVO createReqVO); + + /** + * 更新设备 Modbus 点位配置 + * + * @param updateReqVO 更新信息 + */ + void updateModbusPoint(@Valid IotDeviceModbusPointSaveReqVO updateReqVO); + + /** + * 删除设备 Modbus 点位配置 + * + * @param id 编号 + */ + void deleteModbusPoint(Long id); + + /** + * 获得设备 Modbus 点位配置 + * + * @param id 编号 + * @return 设备 Modbus 点位配置 + */ + IotDeviceModbusPointDO getModbusPoint(Long id); + + /** + * 获得设备 Modbus 点位配置分页 + * + * @param pageReqVO 分页查询 + * @return 设备 Modbus 点位配置分页 + */ + PageResult getModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO); + + /** + * 根据设备编号获得点位配置列表 + * + * @param deviceId 设备编号 + * @return 点位配置列表 + */ + List getModbusPointListByDeviceId(Long deviceId); + + /** + * 根据设备编号获得启用的点位配置列表 + * + * @param deviceId 设备编号 + * @return 启用的点位配置列表 + */ + List getEnabledModbusPointListByDeviceId(Long deviceId); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java new file mode 100644 index 0000000000..0b79be828b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java @@ -0,0 +1,138 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusPointMapper; +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +/** + * IoT 设备 Modbus 点位配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointService { + + @Resource + private IotDeviceModbusPointMapper modbusPointMapper; + + @Resource + private IotDeviceService deviceService; + + @Resource + private IotThingModelService thingModelService; + + @Override + public Long createModbusPoint(IotDeviceModbusPointSaveReqVO createReqVO) { + // 1.1 校验设备存在 + deviceService.validateDeviceExists(createReqVO.getDeviceId()); + // 1.2 校验物模型属性存在 + IotThingModelDO thingModel = validateThingModelExists(createReqVO.getThingModelId()); + // 1.3 校验同一设备下点位唯一性(基于 identifier) + validateModbusPointUnique(createReqVO.getDeviceId(), thingModel.getIdentifier(), null); + + // 2. 插入 + IotDeviceModbusPointDO modbusPoint = BeanUtils.toBean(createReqVO, IotDeviceModbusPointDO.class, + o -> o.setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName())); + modbusPointMapper.insert(modbusPoint); + return modbusPoint.getId(); + } + + @Override + public void updateModbusPoint(IotDeviceModbusPointSaveReqVO updateReqVO) { + // 1.1 校验存在 + validateModbusPointExists(updateReqVO.getId()); + // 1.2 校验设备存在 + deviceService.validateDeviceExists(updateReqVO.getDeviceId()); + // 1.3 校验物模型属性存在 + IotThingModelDO thingModel = validateThingModelExists(updateReqVO.getThingModelId()); + // 1.4 校验同一设备下点位唯一性 + validateModbusPointUnique(updateReqVO.getDeviceId(), thingModel.getIdentifier(), updateReqVO.getId()); + + // 2. 更新 + IotDeviceModbusPointDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusPointDO.class); + // TODO @AI:这块 + modbusPointMapper.updateById(updateObj); + } + + private IotThingModelDO validateThingModelExists(Long id) { + IotThingModelDO thingModel = thingModelService.getThingModel(id); + if (thingModel == null) { + throw exception(THING_MODEL_NOT_EXISTS); + } + return thingModel; + } + + @Override + public void deleteModbusPoint(Long id) { + // 校验存在 + validateModbusPointExists(id); + // 删除 + modbusPointMapper.deleteById(id); + } + + private IotDeviceModbusPointDO validateModbusPointExists(Long id) { + IotDeviceModbusPointDO point = modbusPointMapper.selectById(id); + if (point == null) { + throw exception(DEVICE_MODBUS_POINT_NOT_EXISTS); + } + return point; + } + + private void validateModbusPointUnique(Long deviceId, String identifier, Long excludeId) { + IotDeviceModbusPointDO point = modbusPointMapper.selectByDeviceIdAndIdentifier(deviceId, identifier); + // TODO @AI:ObjUtil notequals; + if (point != null && !point.getId().equals(excludeId)) { + throw exception(DEVICE_MODBUS_POINT_EXISTS); + } + } + + // TODO @AI:这块 + private void setDefaultValues(IotDeviceModbusPointDO point) { + if (point.getRegisterCount() == null) { + point.setRegisterCount(1); + } + if (point.getScale() == null) { + point.setScale(BigDecimal.ONE); + } + if (point.getPollInterval() == null) { + point.setPollInterval(5000); + } + } + + @Override + public IotDeviceModbusPointDO getModbusPoint(Long id) { + return modbusPointMapper.selectById(id); + } + + @Override + public PageResult getModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO) { + return modbusPointMapper.selectPage(pageReqVO); + } + + @Override + public List getModbusPointListByDeviceId(Long deviceId) { + return modbusPointMapper.selectListByDeviceId(deviceId); + } + + @Override + public List getEnabledModbusPointListByDeviceId(Long deviceId) { + return modbusPointMapper.selectListByDeviceIdAndStatus(deviceId, CommonStatusEnum.ENABLE.getStatus()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java index 29d540e73e..5dfbed08e1 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java @@ -4,6 +4,9 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; + +import java.util.List; /** * IoT 设备通用 API @@ -28,4 +31,11 @@ public interface IotDeviceCommonApi { */ CommonResult getDevice(IotDeviceGetReqDTO infoReqDTO); + /** + * 获取所有启用的 Modbus 设备配置列表 + * + * @return Modbus 设备配置列表 + */ + CommonResult> getEnabledModbusDeviceConfigs(); + } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java new file mode 100644 index 0000000000..46d318a95e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.iot.core.biz.dto; + +import lombok.Data; + +import java.util.List; + +/** + * IoT Modbus 设备配置 Response DTO + * + * @author 芋道源码 + */ +@Data +public class IotModbusDeviceConfigRespDTO { + + /** + * 设备编号 + */ + private Long deviceId; + /** + * 产品标识 + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; + /** + * 租户编号 + */ + private Long tenantId; + + // ========== Modbus 连接配置 ========== + + /** + * Modbus 服务器 IP 地址 + */ + private String ip; + /** + * Modbus 端口 + */ + private Integer port; + /** + * 从站地址 + */ + private Integer slaveId; + /** + * 连接超时时间(毫秒) + */ + private Integer timeout; + /** + * 重试间隔(毫秒) + */ + private Integer retryInterval; + + // ========== 点位配置 ========== + + /** + * 点位列表 + */ + private List points; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java new file mode 100644 index 0000000000..26596c5e36 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.iot.core.biz.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * IoT Modbus 点位配置 Response DTO + * + * @author 芋道源码 + */ +@Data +public class IotModbusPointRespDTO { + + /** + * 点位编号 + */ + private Long id; + /** + * 属性标识符(物模型的 identifier) + */ + private String identifier; + /** + * 属性名称(物模型的 name) + */ + private String name; + + // ========== Modbus 协议配置 ========== + + // TODO @AI:所有的枚举,通过 @,不要写上去; + /** + * Modbus 功能码 + * + * 1-ReadCoils 2-ReadDiscreteInputs 3-ReadHoldingRegisters 4-ReadInputRegisters + */ + private Integer functionCode; + /** + * 寄存器起始地址 + */ + private Integer registerAddress; + /** + * 寄存器数量 + */ + private Integer registerCount; + /** + * 字节序 + * + * AB/BA/ABCD/CDAB/DCBA/BADC + */ + private String byteOrder; + /** + * 原始数据类型 + * + * INT16/UINT16/INT32/UINT32/FLOAT/DOUBLE/BOOLEAN/STRING + */ + private String rawDataType; + /** + * 缩放因子 + */ + private BigDecimal scale; + /** + * 轮询间隔(毫秒) + */ + private Integer pollInterval; + + // ========== 物模型相关字段 ========== + + // TODO @AI:分析一下,是否有必要返回 + /** + * 数据类型(来自物模型) + */ + private String dataType; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/pom.xml b/yudao-module-iot/yudao-module-iot-gateway/pom.xml index 8fde9dc3ce..5d76c59fd0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/pom.xml +++ b/yudao-module-iot/yudao-module-iot-gateway/pom.xml @@ -33,7 +33,7 @@ org.apache.rocketmq rocketmq-spring-boot-starter - + true @@ -48,6 +48,13 @@ vertx-mqtt + + + com.ghgande + j2mod + 3.2.1 + + cn.iocoder.boot diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index 3e573efdde..53aafda08b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscr import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.*; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; @@ -194,4 +195,38 @@ public class IotGatewayConfiguration { } + /** + * IoT 网关 Modbus TCP 协议配置类 + */ + @Configuration + @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.modbus-tcp", name = "enabled", havingValue = "true") + @Slf4j + public static class ModbusTcpProtocolConfiguration { + + @Bean(name = "modbusTcpVertx", destroyMethod = "close") + public Vertx modbusTcpVertx() { + return Vertx.vertx(); + } + + @Bean + public IotModbusTcpUpstreamProtocol iotModbusTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, + IotDeviceMessageService messageService, + IotModbusTcpConnectionManager connectionManager, + IotModbusTcpPollScheduler pollScheduler, + IotModbusTcpConfigCacheService configCacheService, + IotModbusTcpUpstreamHandler upstreamHandler, + @Qualifier("modbusTcpVertx") Vertx modbusTcpVertx) { + return new IotModbusTcpUpstreamProtocol(gatewayProperties.getProtocol().getModbusTcp(), + messageService, connectionManager, pollScheduler, configCacheService, upstreamHandler, modbusTcpVertx); + } + + @Bean + public IotModbusTcpDownstreamSubscriber iotModbusTcpDownstreamSubscriber(IotModbusTcpUpstreamProtocol upstreamProtocol, + IotModbusTcpDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + return new IotModbusTcpDownstreamSubscriber(upstreamProtocol, downstreamHandler, messageBus); + } + + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 7655a3759e..5778003269 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -93,6 +93,11 @@ public class IotGatewayProperties { */ private MqttWsProperties mqttWs; + /** + * Modbus TCP 组件配置 + */ + private ModbusTcpProperties modbusTcp; + } @Data @@ -503,4 +508,21 @@ public class IotGatewayProperties { } + @Data + public static class ModbusTcpProperties { + + /** + * 是否开启 + */ + @NotNull(message = "是否开启不能为空") + private Boolean enabled; + + /** + * 配置刷新间隔(秒) + */ + // TODO @AI:需要校验下非空; + private Integer configRefreshInterval = 30; + + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java new file mode 100644 index 0000000000..ddec584bb1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java @@ -0,0 +1,281 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// TODO @AI:注释上:希望 1 和 2,然后 3 只是辅助说明 +/** + * IoT Modbus 数据转换器 + * + * 负责: + * 1. 将 Modbus 原始寄存器值转换为物模型属性值 + * 2. 将物模型属性值转换为 Modbus 原始寄存器值 + * 3. 处理字节序、数据类型、缩放因子 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Component +@Slf4j +public class IotModbusDataConverter { + + /** + * 将原始值转换为物模型属性值 + * + * @param rawValues 原始值数组(寄存器值或线圈值) + * @param point 点位配置 + * @return 转换后的属性值 + */ + public Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) { + // TODO @AI:CollUtil.isEmpty; + if (rawValues == null || rawValues.length == 0) { + return null; + } + String rawDataType = point.getRawDataType(); + String byteOrder = point.getByteOrder(); + // TODO @AI:defaultIfNull; + BigDecimal scale = point.getScale() != null ? point.getScale() : BigDecimal.ONE; + + // 1. 根据原始数据类型解析原始数值 + Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder); + if (rawNumber == null) { + return null; + } + + // 2. 应用缩放因子:实际值 = 原始值 × scale + BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale); + + // 3. 根据数据类型返回合适的 Java 类型 + return formatValue(actualValue, rawDataType); + } + + /** + * 将物模型属性值转换为原始寄存器值 + * + * @param propertyValue 属性值 + * @param point 点位配置 + * @return 原始值数组 + */ + public int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) { + if (propertyValue == null) { + return new int[0]; + } + String rawDataType = point.getRawDataType(); + String byteOrder = point.getByteOrder(); + BigDecimal scale = point.getScale() != null ? point.getScale() : BigDecimal.ONE; + int registerCount = point.getRegisterCount() != null ? point.getRegisterCount() : 1; + + // 1. 转换为 BigDecimal + BigDecimal actualValue = new BigDecimal(propertyValue.toString()); + + // 2. 应用缩放因子:原始值 = 实际值 ÷ scale + BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP); + + // 3. 根据原始数据类型编码为寄存器值 + return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount); + } + + /** + * 解析原始值 + */ + private Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) { + // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的; + switch (rawDataType.toUpperCase()) { + case "BOOLEAN": + return rawValues[0] != 0 ? 1 : 0; + case "INT16": + return (short) rawValues[0]; + case "UINT16": + return rawValues[0] & 0xFFFF; + case "INT32": + return parseInt32(rawValues, byteOrder); + case "UINT32": + return parseUint32(rawValues, byteOrder); + case "FLOAT": + return parseFloat(rawValues, byteOrder); + case "DOUBLE": + return parseDouble(rawValues, byteOrder); + // TODO @AI:未知抛出异常; + default: + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); + return rawValues[0]; + } + } + + // TODO @AI:这些转换,有没一些工具类,可以优化;类似 hutool 的? + private int parseInt32(int[] rawValues, String byteOrder) { + if (rawValues.length < 2) { + return rawValues[0]; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt(); + } + + private long parseUint32(int[] rawValues, String byteOrder) { + if (rawValues.length < 2) { + return rawValues[0] & 0xFFFFFFFFL; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL; + } + + private float parseFloat(int[] rawValues, String byteOrder) { + if (rawValues.length < 2) { + return (float) rawValues[0]; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat(); + } + + private double parseDouble(int[] rawValues, String byteOrder) { + if (rawValues.length < 4) { + return rawValues[0]; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble(); + } + + /** + * 将寄存器值转换为字节数组 + */ + private byte[] registersToBytes(int[] registers, int count) { + byte[] bytes = new byte[count * 2]; + for (int i = 0; i < Math.min(registers.length, count); i++) { + bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF); + bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF); + } + return bytes; + } + + /** + * 根据字节序重排字节 + */ + private byte[] reorderBytes(byte[] bytes, String byteOrder) { + // 大端序,不需要调整 + // TODO @AI:StrUtil.equals;null 要抛出异常; + if (byteOrder == null || "ABCD".equals(byteOrder) || "AB".equals(byteOrder)) { + return bytes; + } + + // 其他字节序调整 + byte[] result = new byte[bytes.length]; + switch (byteOrder.toUpperCase()) { + // TODO @AI:走枚举;sortOrder; + case "BA": // 小端序(16 位) + if (bytes.length >= 2) { + result[0] = bytes[1]; + result[1] = bytes[0]; + } + break; + case "CDAB": // 大端字交换(32 位) + if (bytes.length >= 4) { + result[0] = bytes[2]; + result[1] = bytes[3]; + result[2] = bytes[0]; + result[3] = bytes[1]; + } + break; + case "DCBA": // 小端序(32 位) + if (bytes.length >= 4) { + result[0] = bytes[3]; + result[1] = bytes[2]; + result[2] = bytes[1]; + result[3] = bytes[0]; + } + break; + case "BADC": // 小端字交换(32 位) + if (bytes.length >= 4) { + result[0] = bytes[1]; + result[1] = bytes[0]; + result[2] = bytes[3]; + result[3] = bytes[2]; + } + break; + // TODO @AI:未知就抛出异常; + default: + return bytes; + } + return result; + } + + /** + * 编码为寄存器值 + */ + private int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) { + // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的; + switch (rawDataType.toUpperCase()) { + case "BOOLEAN": + return new int[]{rawValue.intValue() != 0 ? 1 : 0}; + case "INT16": + case "UINT16": + return new int[]{rawValue.intValue() & 0xFFFF}; + case "INT32": + case "UINT32": + return encodeInt32(rawValue.intValue(), byteOrder); + case "FLOAT": + return encodeFloat(rawValue.floatValue(), byteOrder); + case "DOUBLE": + return encodeDouble(rawValue.doubleValue(), byteOrder); + default: + return new int[]{rawValue.intValue()}; + } + } + + // TODO @AI:这些转换,有没一些工具类,可以优化;类似 hutool 的? + private int[] encodeInt32(int value, String byteOrder) { + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array(); + bytes = reorderBytes(bytes, byteOrder); + return bytesToRegisters(bytes); + } + + private int[] encodeFloat(float value, String byteOrder) { + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array(); + bytes = reorderBytes(bytes, byteOrder); + return bytesToRegisters(bytes); + } + + private int[] encodeDouble(double value, String byteOrder) { + byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array(); + bytes = reorderBytes(bytes, byteOrder); + return bytesToRegisters(bytes); + } + + private int[] bytesToRegisters(byte[] bytes) { + int[] registers = new int[bytes.length / 2]; + for (int i = 0; i < registers.length; i++) { + registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF); + } + return registers; + } + + /** + * 格式化返回值 + */ + private Object formatValue(BigDecimal value, String rawDataType) { + // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的; + switch (rawDataType.toUpperCase()) { + case "BOOLEAN": + return value.intValue() != 0; + case "INT16": + case "INT32": + return value.intValue(); + case "UINT16": + case "UINT32": + return value.longValue(); + case "FLOAT": + return value.floatValue(); + case "DOUBLE": + return value.doubleValue(); + // TODO @AI:未知抛出异常; + default: + return value; + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java new file mode 100644 index 0000000000..43a2fbee47 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java @@ -0,0 +1,195 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; +import com.ghgande.j2mod.modbus.msg.*; +import com.ghgande.j2mod.modbus.procimg.InputRegister; +import com.ghgande.j2mod.modbus.procimg.Register; +import com.ghgande.j2mod.modbus.procimg.SimpleRegister; +import com.ghgande.j2mod.modbus.util.BitVector; +import io.vertx.core.Future; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * IoT Modbus TCP 客户端 + * + * 负责: + * 1. 封装 Modbus 读写操作 + * 2. 根据功能码执行对应的 Modbus 请求 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Component +@RequiredArgsConstructor // TODO @AI:这个注解,是不是可以去掉? +@Slf4j +public class IotModbusTcpClient { + + /** + * 读取 Modbus 数据 + * + * @param connection Modbus 连接 + * @param slaveId 从站地址 + * @param point 点位配置 + * @return 原始值(int 数组) + */ + public Future read(IotModbusTcpConnectionManager.ModbusConnection connection, + Integer slaveId, + IotModbusPointRespDTO point) { + return connection.executeBlocking(tcpConnection -> { + try { + // 1. 创建请求 + ModbusRequest request = createReadRequest(point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount()); + request.setUnitID(slaveId); + + // 2. 执行事务(请求) + ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection); + transaction.setRequest(request); + transaction.execute(); + + // 3. 解析响应 + ModbusResponse response = transaction.getResponse(); + return extractValues(response, point.getFunctionCode()); + } catch (Exception e) { + // TODO @AI:抛出异常时,增加更多的上下文信息,比如设备、点位等 + throw new RuntimeException("Modbus 读取失败", e); + } + }); + } + + /** + * 写入 Modbus 数据 + * + * @param connection Modbus 连接 + * @param slaveId 从站地址 + * @param point 点位配置 + * @param values 要写入的值 + * @return 是否成功 + */ + public Future write(IotModbusTcpConnectionManager.ModbusConnection connection, + Integer slaveId, + IotModbusPointRespDTO point, + int[] values) { + return connection.executeBlocking(tcpConnection -> { + try { + // 1. 创建请求 + ModbusRequest request = createWriteRequest(point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount(), values); + if (request == null) { + throw new RuntimeException("功能码 " + point.getFunctionCode() + " 不支持写操作"); + } + request.setUnitID(slaveId); + + // 2. 执行事务(请求) + ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection); + transaction.setRequest(request); + transaction.execute(); + return true; + } catch (Exception e) { + // TODO @AI:抛出异常时,增加更多的上下文信息,比如设备、点位等; + throw new RuntimeException("Modbus 写入失败", e); + } + }); + } + + /** + * 创建读取请求 + */ + private ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { + // TODO @AI:1、2、3、4 能不能有枚举哈?这样 1、2、3、4 那的注释就不用写; + switch (functionCode) { + case 1: // ReadCoils + return new ReadCoilsRequest(address, count); + case 2: // ReadDiscreteInputs + return new ReadInputDiscretesRequest(address, count); + case 3: // ReadHoldingRegisters + return new ReadMultipleRegistersRequest(address, count); + case 4: // ReadInputRegisters + return new ReadInputRegistersRequest(address, count); + default: + throw new IllegalArgumentException("不支持的功能码: " + functionCode); + } + } + + /** + * 创建写入请求 + */ + private ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { + // TODO @AI:5、6、15、16 能不能有枚举哈?这样 5、6、15、16 那的注释就不用写; + switch (functionCode) { + case 1: // WriteCoils (使用 5 或 15) + if (count == 1) { + return new WriteCoilRequest(address, values[0] != 0); + } else { + // 多线圈写入 + BitVector bv = new BitVector(count); + for (int i = 0; i < Math.min(values.length, count); i++) { + bv.setBit(i, values[i] != 0); + } + return new WriteMultipleCoilsRequest(address, bv); + } + case 3: // WriteHoldingRegisters (使用 6 或 16) + if (count == 1) { + return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0])); + } else { + Register[] registers = new com.ghgande.j2mod.modbus.procimg.SimpleRegister[count]; + for (int i = 0; i < count; i++) { + registers[i] = new SimpleRegister(i < values.length ? values[i] : 0); + } + return new WriteMultipleRegistersRequest(address, registers); + } + case 2: // ReadDiscreteInputs - 只读 + case 4: // ReadInputRegisters - 只读 + return null; + default: + throw new IllegalArgumentException("不支持的功能码: " + functionCode); + } + } + + /** + * 从响应中提取值 + */ + private int[] extractValues(ModbusResponse response, Integer functionCode) { + // TODO @AI:1、2、3、4 能不能有枚举哈?这样 1、2、3、4 那的注释就不用写; + switch (functionCode) { + case 1: // ReadCoils + ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; + int bitCount = coilsResponse.getBitCount(); + int[] coilValues = new int[bitCount]; + for (int i = 0; i < bitCount; i++) { + coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0; + } + return coilValues; + case 2: // ReadDiscreteInputs + ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response; + int discreteCount = discretesResponse.getBitCount(); + int[] discreteValues = new int[discreteCount]; + for (int i = 0; i < discreteCount; i++) { + discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0; + } + return discreteValues; + case 3: // ReadHoldingRegisters + ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response; + InputRegister[] holdingRegisters = holdingResponse.getRegisters(); + int[] holdingValues = new int[holdingRegisters.length]; + for (int i = 0; i < holdingRegisters.length; i++) { + holdingValues[i] = holdingRegisters[i].getValue(); + } + return holdingValues; + case 4: // ReadInputRegisters + ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response; + InputRegister[] inputRegisters = inputResponse.getRegisters(); + int[] inputValues = new int[inputRegisters.length]; + for (int i = 0; i < inputRegisters.length; i++) { + inputValues[i] = inputRegisters[i].getValue(); + } + return inputValues; + default: + throw new IllegalArgumentException("不支持的功能码: " + functionCode); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java new file mode 100644 index 0000000000..38744bf164 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +// TODO @AI:是不是 1、2、3 注释可以合并下; +/** + * IoT Modbus TCP 配置缓存服务 + * + * 负责: + * 1. 从 biz 拉取 Modbus 设备配置 + * 2. 缓存配置数据 + * 3. 检测配置变更 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Service +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpConfigCacheService { + + private final IotDeviceCommonApi deviceApi; + + /** + * 配置缓存:deviceId -> 配置 + */ + private final Map configCache = new ConcurrentHashMap<>(); + + /** + * 已知的设备 ID 集合 + */ + private final Set knownDeviceIds = ConcurrentHashMap.newKeySet(); + + /** + * 刷新配置 + * + * @return 最新的配置列表 + */ + public List refreshConfig() { + try { + // 1. 从远程获取配置 + CommonResult> result = deviceApi.getEnabledModbusDeviceConfigs(); + if (result == null || !result.isSuccess() || result.getData() == null) { + log.warn("[refreshConfig][获取 Modbus 配置失败: {}]", result); + return new ArrayList<>(configCache.values()); + } + List configs = result.getData(); + + // 2. 更新缓存 + for (IotModbusDeviceConfigRespDTO config : configs) { + configCache.put(config.getDeviceId(), config); + knownDeviceIds.add(config.getDeviceId()); + } + return configs; + } catch (Exception e) { + log.error("[refreshConfig][刷新配置失败]", e); + return new ArrayList<>(configCache.values()); + } + } + + /** + * 获取设备配置 + * + * @param deviceId 设备 ID + * @return 配置 + */ + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { + return configCache.get(deviceId); + } + + /** + * 清理已删除设备的资源 + * + * @param currentConfigs 当前有效的配置列表 + * @param cleanupAction 清理动作 + */ + public void cleanupRemovedDevices(List currentConfigs, Consumer cleanupAction) { + // 1.1 获取当前有效的设备 ID + // TODO @AI:convertSet 简化; + Set currentDeviceIds = new HashSet<>(); + for (IotModbusDeviceConfigRespDTO config : currentConfigs) { + currentDeviceIds.add(config.getDeviceId()); + } + // 1.2 找出已删除的设备 + Set removedDeviceIds = new HashSet<>(knownDeviceIds); + removedDeviceIds.removeAll(currentDeviceIds); + + // 2. 清理已删除设备 + for (Long deviceId : removedDeviceIds) { + log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); + configCache.remove(deviceId); + knownDeviceIds.remove(deviceId); + cleanupAction.accept(deviceId); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java new file mode 100644 index 0000000000..75b98ea381 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java @@ -0,0 +1,255 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import com.ghgande.j2mod.modbus.net.TCPMasterConnection; +import io.vertx.core.Context; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Component; + +import java.net.InetAddress; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT Modbus TCP 连接管理器 + * + * 负责: + * 1. 管理 TCP 连接(相同 ip:port 共用连接) + * 2. 分布式锁管理(连接级别) + * 3. 连接重试和故障恢复 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Component +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpConnectionManager { + + // TODO @AI:iot:modbus-tcp:connection: + private static final String LOCK_KEY_PREFIX = "iot:modbus:connection:"; + + private final RedissonClient redissonClient; + private final Vertx vertx; + + /** + * 连接池:key = ip:port + */ + private final Map connectionPool = new ConcurrentHashMap<>(); + + /** + * 设备 ID 到连接 key 的映射 + */ + private final Map deviceConnectionMap = new ConcurrentHashMap<>(); + + /** + * 确保连接存在 + * + * @param config 设备配置 + */ + public void ensureConnection(IotModbusDeviceConfigRespDTO config) { + // 1. 记录设备与连接的关系 + String connectionKey = buildConnectionKey(config.getIp(), config.getPort()); + deviceConnectionMap.put(config.getDeviceId(), connectionKey); + + // 2. 情况一:连接已存在,添加设备引用 + ModbusConnection connection = connectionPool.get(connectionKey); + if (connection != null) { + // 添加设备引用 + connection.addDevice(config.getDeviceId(), config.getSlaveId()); + // 更新连接参数(取最小值) + // TODO @AI:(不确定)如果后续最小值被移除后,是不是无法灰度到上一个最小值? + connection.updateParams(config.getTimeout(), config.getRetryInterval()); + return; + } + + // 3. 情况二:连接不存在,创建新连接 + // 3.1 尝试获取分布式锁 + RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + connectionKey); + if (!lock.tryLock()) { + log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey); + return; + } + // 3.2 创建新连接 + try { + connection = createConnection(config, lock); + connectionPool.put(connectionKey, connection); + log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey); + } catch (Exception e) { + log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e); + lock.unlock(); + } + } + + /** + * 创建 Modbus TCP 连接 + */ + private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config, RLock lock) throws Exception { + // 创建 TCP 连接 + // TODO @AI:需要重连么? + TCPMasterConnection tcpConnection = new TCPMasterConnection(InetAddress.getByName(config.getIp())); + tcpConnection.setPort(config.getPort()); + tcpConnection.setTimeout(config.getTimeout()); + tcpConnection.connect(); + + // 创建 Modbus 连接对象 + ModbusConnection connection = new ModbusConnection(); + // TODO @AI:链式调用,简化下; + connection.setConnectionKey(buildConnectionKey(config.getIp(), config.getPort())); + connection.setTcpConnection(tcpConnection); + connection.setLock(lock); + connection.setTimeout(config.getTimeout()); + connection.setRetryInterval(config.getRetryInterval()); + connection.setContext(vertx.getOrCreateContext()); + connection.addDevice(config.getDeviceId(), config.getSlaveId()); + return connection; + } + + /** + * 获取连接 + */ + public ModbusConnection getConnection(Long deviceId) { + String connectionKey = deviceConnectionMap.get(deviceId); + if (connectionKey == null) { + return null; + } + return connectionPool.get(connectionKey); + } + + /** + * 获取设备的 slave ID + */ + public Integer getSlaveId(Long deviceId) { + ModbusConnection connection = getConnection(deviceId); + if (connection == null) { + return null; + } + return connection.getSlaveId(deviceId); + } + + /** + * 移除设备 + */ + public void removeDevice(Long deviceId) { + // 1. 移除设备引用 + String connectionKey = deviceConnectionMap.remove(deviceId); + if (connectionKey == null) { + return; + } + ModbusConnection connection = connectionPool.get(connectionKey); + if (connection == null) { + return; + } + connection.removeDevice(deviceId); + + // 2. 如果没有设备引用了,关闭连接 + if (connection.getDeviceCount() == 0) { + closeConnection(connectionKey); + } + } + + /** + * 关闭指定连接 + */ + private void closeConnection(String connectionKey) { + ModbusConnection connection = connectionPool.remove(connectionKey); + if (connection == null) { + return; + } + + try { + if (connection.getTcpConnection() != null) { + connection.getTcpConnection().close(); + } + // TODO @AI:(不确定)是不是要当前线程?还是当前进程就 ok 了。 + if (connection.getLock() != null && connection.getLock().isHeldByCurrentThread()) { + connection.getLock().unlock(); + } + log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey); + } catch (Exception e) { + log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e); + } + } + + /** + * 关闭所有连接 + */ + public void closeAll() { + for (String connectionKey : connectionPool.keySet()) { + closeConnection(connectionKey); + } + deviceConnectionMap.clear(); + } + + private String buildConnectionKey(String ip, Integer port) { + return ip + ":" + port; + } + + /** + * Modbus 连接信息 + */ + @Data + public static class ModbusConnection { + + private String connectionKey; + private TCPMasterConnection tcpConnection; + private RLock lock; + private Integer timeout; + private Integer retryInterval; + private Context context; + + /** + * 设备 ID 到 slave ID 的映射 + */ + private final Map deviceSlaveMap = new ConcurrentHashMap<>(); + + public void addDevice(Long deviceId, Integer slaveId) { + deviceSlaveMap.put(deviceId, slaveId); + } + + public void removeDevice(Long deviceId) { + deviceSlaveMap.remove(deviceId); + } + + public int getDeviceCount() { + return deviceSlaveMap.size(); + } + + public Integer getSlaveId(Long deviceId) { + return deviceSlaveMap.get(deviceId); + } + + public void updateParams(Integer timeout, Integer retryInterval) { + // 取最小值 + if (timeout != null && (this.timeout == null || timeout < this.timeout)) { + this.timeout = timeout; + } + if (retryInterval != null && (this.retryInterval == null || retryInterval < this.retryInterval)) { + this.retryInterval = retryInterval; + } + } + + /** + * 执行 Modbus 读取操作(阻塞方式,在 Vert.x worker 线程执行) + */ + public Future executeBlocking(java.util.function.Function operation) { + // TODO @AI:executeBlocking 方法,已经废弃了,看看要不要替换。 + // TODO @AI:疑问。这里会不会线程阻塞?多个设备之间,担心性能; + return context.executeBlocking(promise -> { + try { + T result = operation.apply(tcpConnection); + promise.complete(result); + } catch (Exception e) { + promise.fail(e); + } + }, true); // ordered=true 保证同一连接串行执行 + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java new file mode 100644 index 0000000000..eef237fa44 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java @@ -0,0 +1,128 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; + +// TODO @AI:看看注释能不能优化下; +/** + * IoT Modbus TCP 下行消息处理器 + * + * 负责: + * 1. 处理属性设置消息(thing.service.property.set) + * 2. 将属性值转换为 Modbus 原始值 + * 3. 执行 Modbus 写入操作 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Component +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpDownstreamHandler { + + private final IotModbusTcpConnectionManager connectionManager; + private final IotModbusTcpClient modbusClient; + private final IotModbusDataConverter dataConverter; + private final IotModbusTcpConfigCacheService configCacheService; + + /** + * 处理下行消息 + */ + @SuppressWarnings("unchecked") + public void handle(IotDeviceMessage message) { + // 1.1 检查是否是属性设置消息 + if (!"thing.service.property.set".equals(message.getMethod())) { + log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); + return; + } + // 1.2 获取设备配置 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId()); + if (config == null) { + log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId()); + return; + } + + // 2. 解析属性值并写入 + Object params = message.getParams(); + if (!(params instanceof Map)) { + log.warn("[handle][params 不是 Map 类型: {}]", params); + return; + } + Map propertyMap = (Map) params; + for (Map.Entry entry : propertyMap.entrySet()) { + String identifier = entry.getKey(); + Object value = entry.getValue(); + // 2.1.1 查找对应的点位配置 + IotModbusPointRespDTO point = findPoint(config, identifier); + if (point == null) { + log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); + continue; + } + // 2.1.2 检查是否支持写操作 + if (!isWritable(point.getFunctionCode())) { + log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); + continue; + } + + // 2.2 执行写入 + writeProperty(config, point, value); + } + } + + /** + * 写入属性值 + */ + private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) { + // 1.1 获取连接 + IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); + if (connection == null) { + log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId()); + return; + } + // 1.2 获取 slave ID + Integer slaveId = connectionManager.getSlaveId(config.getDeviceId()); + if (slaveId == null) { + log.warn("[writeProperty][设备 {} 没有 slaveId]", config.getDeviceId()); + return; + } + + // 2.1 转换属性值为原始值 + int[] rawValues = dataConverter.convertToRawValues(value, point); + // 2.2 执行 Modbus 写入 + modbusClient.write(connection, slaveId, point, rawValues) + .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", + config.getDeviceId(), point.getIdentifier(), value)) + .onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]", + config.getDeviceId(), point.getIdentifier(), e)); + } + + /** + * 查找点位配置 + */ + private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { + if (config.getPoints() == null) { + return null; + } + // TODO @AI:hutool findOne? + return config.getPoints().stream() + .filter(p -> identifier.equals(p.getIdentifier())) + .findFirst() + .orElse(null); + } + + /** + * 检查功能码是否支持写操作 + */ + private boolean isWritable(Integer functionCode) { + // TODO @AI:能不能通过 枚举优化下? + // 功能码 1(ReadCoils)和 3(ReadHoldingRegisters)支持写操作 + return functionCode != null && (functionCode == 1 || functionCode == 3); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java new file mode 100644 index 0000000000..241faa8de5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT Modbus TCP 下行消息订阅器 + * + * 负责: + * 1. 订阅消息总线的下行消息 + * 2. 将属性设置消息转发给下行处理器 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber { + + private final IotModbusTcpUpstreamProtocol upstreamProtocol; + private final IotModbusTcpDownstreamHandler downstreamHandler; + private final IotMessageBus messageBus; + + @PostConstruct + public void subscribe() { + messageBus.register(this); + log.info("[subscribe][Modbus TCP 下行消息订阅器已启动, topic={}]", getTopic()); + } + + @Override + public String getTopic() { + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId()); + } + + @Override + public String getGroup() { + return getTopic(); // 点对点消费 + } + + @Override + public void onMessage(IotDeviceMessage message) { + log.debug("[onMessage][收到下行消息: {}]", message); + try { + downstreamHandler.handle(message); + } catch (Exception e) { + log.error("[onMessage][处理下行消息失败]", e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java new file mode 100644 index 0000000000..e4b319223d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java @@ -0,0 +1,134 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import io.vertx.core.Vertx; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT Modbus TCP 轮询调度器 + * + * 负责: + * 1. 管理每个点位的轮询定时器 + * 2. 调度 Modbus 读取任务 + * 3. 处理读取结果并上报 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Component +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpPollScheduler { + + private final Vertx vertx; + private final IotModbusTcpConnectionManager connectionManager; + private final IotModbusTcpClient modbusClient; + private final IotModbusTcpUpstreamHandler upstreamHandler; + + /** + * 设备的定时器列表:deviceId -> timerId 列表 + */ + private final Map> deviceTimers = new ConcurrentHashMap<>(); + + /** + * 更新轮询任务 + */ + public void updatePolling(IotModbusDeviceConfigRespDTO config) { + Long deviceId = config.getDeviceId(); + + // 1. 停止旧的轮询任务 + stopPolling(deviceId); + + // 2.1 为每个点位创建新的轮询任务 + List timerIds = new ArrayList<>(); + // TODO @AI:if return 简化;上面的 size 加下;config.getPoints() + if (config.getPoints() != null) { + for (IotModbusPointRespDTO point : config.getPoints()) { + Long timerId = createPollTimer(config, point); + if (timerId != null) { + timerIds.add(timerId); + } + } + } + // 2.2 记录定时器 + // TODO @AI:CollUtil.isNotEmpty;if return 简化; + if (!timerIds.isEmpty()) { + deviceTimers.put(deviceId, timerIds); + log.debug("[updatePolling][设备 {} 创建了 {} 个轮询定时器]", deviceId, timerIds.size()); + } + } + + /** + * 创建轮询定时器 + */ + private Long createPollTimer(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { + if (point.getPollInterval() == null || point.getPollInterval() <= 0) { + return null; + } + return vertx.setPeriodic(point.getPollInterval(), timerId -> { + try { + pollPoint(config, point); + } catch (Exception e) { + log.error("[createPollTimer][轮询点位失败, deviceId={}, identifier={}]", + config.getDeviceId(), point.getIdentifier(), e); + } + }); + } + + /** + * 轮询单个点位 + */ + private void pollPoint(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { + // 1.1 获取连接 + IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); + if (connection == null) { + log.warn("[pollPoint][设备 {} 没有连接]", config.getDeviceId()); + return; + } + // 1.2 获取 slave ID + Integer slaveId = connectionManager.getSlaveId(config.getDeviceId()); + if (slaveId == null) { + log.warn("[pollPoint][设备 {} 没有 slaveId]", config.getDeviceId()); + return; + } + + // 2. 执行 Modbus 读取 + // TODO @AI:超时时间,有实现么??? + modbusClient.read(connection, slaveId, point) + .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue)) + .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]", + config.getDeviceId(), point.getIdentifier(), e)); + } + + /** + * 停止设备的轮询 + */ + public void stopPolling(Long deviceId) { + List timerIds = deviceTimers.remove(deviceId); + // TODO @AI:CollUtil.isNotEmpty;并且 if return; + if (timerIds != null) { + for (Long timerId : timerIds) { + vertx.cancelTimer(timerId); + } + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timerIds.size()); + } + } + + /** + * 停止所有轮询 + */ + public void stopAll() { + for (Long deviceId : deviceTimers.keySet()) { + stopPolling(deviceId); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java new file mode 100644 index 0000000000..df46228244 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * IoT Modbus TCP 上行数据处理器 + * + * 负责: + * 1. 将 Modbus 读取的原始值转换为物模型属性值 + * 2. 构造属性上报消息 + * 3. 发送消息到消息总线 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Component +@Slf4j +public class IotModbusTcpUpstreamHandler { + + private final IotDeviceMessageService messageService; + private final IotModbusDataConverter dataConverter; + + private String serverId; + + public IotModbusTcpUpstreamHandler(IotDeviceMessageService messageService, + IotModbusDataConverter dataConverter) { + this.messageService = messageService; + this.dataConverter = dataConverter; + } + + // TODO @AI:lombok 简化 + /** + * 设置 serverId + * + * @param serverId 服务器 ID + */ + public void setServerId(String serverId) { + this.serverId = serverId; + } + + /** + * 处理 Modbus 读取结果 + * + * @param config 设备配置 + * @param point 点位配置 + * @param rawValue 原始值(int 数组) + */ + public void handleReadResult(IotModbusDeviceConfigRespDTO config, + IotModbusPointRespDTO point, + int[] rawValue) { + try { + // 1.1 转换原始值为物模型属性值 + Object convertedValue = dataConverter.convertToPropertyValue(rawValue, point); + log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]", + config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue); + // 1.2 构造属性上报消息 + Map params = new HashMap<>(); + params.put(point.getIdentifier(), convertedValue); + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.property.post", params); + + // 2. 发送到消息总线 + messageService.sendDeviceMessage(message, config.getProductKey(), + config.getDeviceName(), serverId); + } catch (Exception e) { + log.error("[handleReadResult][处理读取结果失败, deviceId={}, identifier={}]", + config.getDeviceId(), point.getIdentifier(), e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java new file mode 100644 index 0000000000..1717e61171 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java @@ -0,0 +1,128 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * IoT Modbus TCP 上行协议 + * + * 负责: + * 1. 定时从 biz 拉取 Modbus 设备配置 + * 2. 管理 Modbus TCP 连接 + * 3. 调度轮询任务 + * 4. 处理采集数据上报 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpUpstreamProtocol { + + private final IotGatewayProperties.ModbusTcpProperties modbusTcpProperties; + private final IotDeviceMessageService messageService; + private final IotModbusTcpConnectionManager connectionManager; + private final IotModbusTcpPollScheduler pollScheduler; + private final IotModbusTcpConfigCacheService configCacheService; + private final IotModbusTcpUpstreamHandler upstreamHandler; + private final Vertx vertx; + + // TODO @AI:按照别的模块,生成 serverId; + /** + * 服务器 ID,用于标识当前网关实例 + */ + private final String serverId = UUID.randomUUID().toString(); + + /** + * 配置刷新定时器 ID + */ + private Long configRefreshTimerId; + + @PostConstruct + public void start() { + log.info("[start][Modbus TCP 协议启动, serverId={}]", serverId); + + // 0. 设置 serverId 到上行处理器 + upstreamHandler.setServerId(serverId); + + // 1. 首次加载配置 + refreshConfig(); + + // 2. 启动配置定时刷新 + int refreshInterval = modbusTcpProperties.getConfigRefreshInterval(); + configRefreshTimerId = vertx.setPeriodic( + TimeUnit.SECONDS.toMillis(refreshInterval), + id -> refreshConfig() + ); + log.info("[start][配置刷新定时器已启动, 间隔={}秒]", refreshInterval); + } + + @PreDestroy + public void stop() { + log.info("[stop][Modbus TCP 协议停止]"); + + // 1. 取消配置刷新定时器 + if (configRefreshTimerId != null) { + vertx.cancelTimer(configRefreshTimerId); + } + + // 2. 停止轮询调度器 + pollScheduler.stopAll(); + + // 3. 关闭所有连接 + connectionManager.closeAll(); + } + + /** + * 刷新配置 + */ + private void refreshConfig() { + try { + // 1. 从 biz 拉取最新配置 + List configs = configCacheService.refreshConfig(); + log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); + + // 2. 更新连接和轮询任务 + for (IotModbusDeviceConfigRespDTO config : configs) { + try { + // 2.1 确保连接存在 + connectionManager.ensureConnection(config); + // 2.2 更新轮询任务 + // TODO @AI:【重要】如果点位配置没变化,是不是不用 update? + pollScheduler.updatePolling(config); + } catch (Exception e) { + log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); + } + } + + // 3. 清理已删除设备的资源 + configCacheService.cleanupRemovedDevices(configs, deviceId -> { + pollScheduler.stopPolling(deviceId); + connectionManager.removeDevice(deviceId); + }); + } catch (Exception e) { + log.error("[refreshConfig][刷新配置失败]", e); + } + } + + // TODO @AI:是不是 lombok 简化 + public String getServerId() { + return serverId; + } + + // TODO @AI:不需要;可以删除; + public IotDeviceMessageService getMessageService() { + return messageService; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java index b325103743..86940b2ec5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; @@ -18,6 +19,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import java.util.List; + import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; /** @@ -33,6 +36,7 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { private IotGatewayProperties gatewayProperties; private RestTemplate restTemplate; + private RestTemplate modbusRestTemplate; @PostConstruct public void init() { @@ -42,24 +46,35 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { .readTimeout(rpc.getReadTimeout()) .connectTimeout(rpc.getConnectTimeout()) .build(); + // TODO @AI:继续复用 rpc.getConnectTimeout();不需要独立 modbusRestTemplate + modbusRestTemplate = new RestTemplateBuilder() + .rootUri(rpc.getUrl() + "/rpc-api/iot/modbus") + .readTimeout(rpc.getReadTimeout()) + .connectTimeout(rpc.getConnectTimeout()) + .build(); } @Override public CommonResult authDevice(IotDeviceAuthReqDTO authReqDTO) { - return doPost("/auth", authReqDTO, new ParameterizedTypeReference<>() { }); + return doPost(restTemplate, "/auth", authReqDTO, new ParameterizedTypeReference<>() { }); } @Override public CommonResult getDevice(IotDeviceGetReqDTO getReqDTO) { - return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { }); + return doPost(restTemplate, "/get", getReqDTO, new ParameterizedTypeReference<>() { }); } - private CommonResult doPost(String url, T body, + @Override + public CommonResult> getEnabledModbusDeviceConfigs() { + return doPost(modbusRestTemplate, "/enabled-configs", null, new ParameterizedTypeReference<>() { }); + } + + private CommonResult doPost(RestTemplate template, String url, T body, ParameterizedTypeReference> responseType) { try { // 请求 HttpEntity requestEntity = new HttpEntity<>(body); - ResponseEntity> response = restTemplate.exchange( + ResponseEntity> response = template.exchange( url, HttpMethod.POST, requestEntity, responseType); // 响应 CommonResult result = response.getBody(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index f633f1c60b..8ccdcb5b10 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -117,6 +117,9 @@ yudao: keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒) ssl-enabled: false # 是否启用 SSL(wss://) sub-protocol: mqtt # WebSocket 子协议 + # TODO @AI:modbus-tcp 组件配置待补充 + modbus-tcp: + enabled: true --- #################### 日志相关配置 #################### From cece79d04cdf58c579fb5ec6c17219bb380c2a34 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 17 Jan 2026 01:13:06 +0800 Subject: [PATCH 02/53] =?UTF-8?q?feat=EF=BC=9A=E3=80=90iot=E3=80=91modbus-?= =?UTF-8?q?tcp=20=E5=8D=8F=E8=AE=AE=E6=8E=A5=E5=85=A5=EF=BC=9A20%=20?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=EF=BC=9A=E5=9F=BA=E4=BA=8E=20hummin?= =?UTF-8?q?g-beaming-dove.md=20=E8=A7=84=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/api/device/IoTDeviceApiImpl.java | 46 +++++------ .../IotDeviceModbusConfigController.java | 49 +++++------- .../IotDeviceModbusPointController.java | 32 +++----- .../IotDeviceModbusConfigSaveReqVO.java | 3 - .../device/IotDeviceModbusPointMapper.java | 21 ++--- .../enums/device/IotModbusByteOrderEnum.java | 1 + .../device/IotModbusFunctionCodeEnum.java | 15 +--- .../device/IotModbusRawDataTypeEnum.java | 1 + .../device/IotDeviceModbusConfigService.java | 24 ++---- .../IotDeviceModbusConfigServiceImpl.java | 77 +++++-------------- .../device/IotDeviceModbusPointService.java | 28 +++---- .../IotDeviceModbusPointServiceImpl.java | 63 +++++++-------- .../biz/dto/IotModbusDeviceConfigRespDTO.java | 10 +-- .../config/IotGatewayConfiguration.java | 49 ++++++++++++ .../modbustcp/IotModbusDataConverter.java | 37 ++++----- .../modbustcp/IotModbusTcpClient.java | 25 +++--- .../IotModbusTcpConfigCacheService.java | 20 ++--- .../IotModbusTcpConnectionManager.java | 21 +---- .../IotModbusTcpDownstreamHandler.java | 9 +-- .../modbustcp/IotModbusTcpPollScheduler.java | 4 +- .../IotModbusTcpUpstreamHandler.java | 16 +--- .../IotModbusTcpUpstreamProtocol.java | 15 +--- 22 files changed, 216 insertions(+), 350 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java index 5a01518995..4a18802adc 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.api.device; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.enums.RpcConstants; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -23,8 +24,11 @@ import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Set; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; /** * IoT 设备 API 实现类 @@ -71,40 +75,32 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { @PermitAll public CommonResult> getEnabledModbusDeviceConfigs() { // 1. 获取所有启用的 Modbus 连接配置 - List configList = modbusConfigService.getEnabledModbusConfigList(); - if (configList.isEmpty()) { + List configList = modbusConfigService.getEnabledDeviceModbusConfigList(); + if (CollUtil.isEmpty(configList)) { return success(new ArrayList<>()); } // 2. 组装返回结果 + Set deviceIds = convertSet(configList, IotDeviceModbusConfigDO::getDeviceId); + Map deviceMap = deviceService.getDeviceMap(deviceIds); + Map> pointMap = modbusPointService.getEnabledDeviceModbusPointMapByDeviceIds(deviceIds); List result = new ArrayList<>(configList.size()); for (IotDeviceModbusConfigDO config : configList) { - // 2.1 获取设备信息 - // TODO @AI:设备需要批量读取;(先暂时不处理) - IotDeviceDO device = deviceService.getDeviceFromCache(config.getDeviceId()); + // 3.1 获取设备信息 + IotDeviceDO device = deviceMap.get(config.getDeviceId()); if (device == null) { continue; } - - // 2.2 获取启用的点位列表 - // TODO @AI:看看是不是批量读取; - List pointList = modbusPointService.getEnabledModbusPointListByDeviceId(config.getDeviceId()); - - // 2.3 构建 DTO - IotModbusDeviceConfigRespDTO dto = new IotModbusDeviceConfigRespDTO(); - dto.setDeviceId(config.getDeviceId()); - // TODO @AI:这个 productKey、deviceName 这个字段,要不要冗余到 IotDeviceModbusConfigDO 里面?(先暂时不处理) - dto.setProductKey(device.getProductKey()); - dto.setDeviceName(device.getDeviceName()); - dto.setTenantId(device.getTenantId()); - // TODO @AI:看看 dto 的转换,能不能通过 beanutils copy - dto.setIp(config.getIp()); - dto.setPort(config.getPort()); - dto.setSlaveId(config.getSlaveId()); - dto.setTimeout(config.getTimeout()); - dto.setRetryInterval(config.getRetryInterval()); - dto.setPoints(BeanUtils.toBean(pointList, IotModbusPointRespDTO.class)); - result.add(dto); + // 3.2 获取启用的点位列表 + List pointList = pointMap.get(config.getDeviceId()); + if (CollUtil.isEmpty(pointList)) { + continue; + } + // 3.3 构建 IotModbusDeviceConfigRespDTO 对象 + IotModbusDeviceConfigRespDTO configDTO = BeanUtils.toBean(config, IotModbusDeviceConfigRespDTO.class, o -> + o.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()) + .setPoints(BeanUtils.toBean(pointList, IotModbusPointRespDTO.class))); + result.add(configDTO); } return success(result); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java index 14648ed9a5..cbc973e191 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java @@ -28,19 +28,11 @@ public class IotDeviceModbusConfigController { @Resource private IotDeviceModbusConfigService modbusConfigService; - // TODO @AI:create 和 update 合并成 save 接口; - @PostMapping("/create") - @Operation(summary = "创建设备 Modbus 连接配置") + @PostMapping("/save") + @Operation(summary = "保存设备 Modbus 连接配置") @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:create')") - public CommonResult createModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO createReqVO) { - return success(modbusConfigService.createModbusConfig(createReqVO)); - } - - @PutMapping("/update") - @Operation(summary = "更新设备 Modbus 连接配置") - @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:update')") - public CommonResult updateModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO updateReqVO) { - modbusConfigService.updateModbusConfig(updateReqVO); + public CommonResult saveDeviceModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO saveReqVO) { + modbusConfigService.saveDeviceModbusConfig(saveReqVO); return success(true); } @@ -48,36 +40,33 @@ public class IotDeviceModbusConfigController { @Operation(summary = "删除设备 Modbus 连接配置") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:delete')") - public CommonResult deleteModbusConfig(@RequestParam("id") Long id) { - modbusConfigService.deleteModbusConfig(id); + public CommonResult deleteDeviceModbusConfig(@RequestParam("id") Long id) { + modbusConfigService.deleteDeviceModbusConfig(id); return success(true); } - // TODO @AI:这个接口改造,支持 id 或者 deviceId;二选一查询; @GetMapping("/get") @Operation(summary = "获得设备 Modbus 连接配置") - @Parameter(name = "id", description = "编号", required = true, example = "1024") + @Parameter(name = "id", description = "编号", example = "1024") + @Parameter(name = "deviceId", description = "设备编号", example = "2048") @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") - public CommonResult getModbusConfig(@RequestParam("id") Long id) { - IotDeviceModbusConfigDO modbusConfig = modbusConfigService.getModbusConfig(id); - return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class)); - } - - // TODO @AI:合并到 getModbusConfig 接口里; - @GetMapping("/get-by-device-id") - @Operation(summary = "根据设备编号获得 Modbus 连接配置") - @Parameter(name = "deviceId", description = "设备编号", required = true, example = "1024") - @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") - public CommonResult getModbusConfigByDeviceId(@RequestParam("deviceId") Long deviceId) { - IotDeviceModbusConfigDO modbusConfig = modbusConfigService.getModbusConfigByDeviceId(deviceId); + public CommonResult getDeviceModbusConfig( + @RequestParam(value = "id", required = false) Long id, + @RequestParam(value = "deviceId", required = false) Long deviceId) { + IotDeviceModbusConfigDO modbusConfig = null; + if (id != null) { + modbusConfig = modbusConfigService.getDeviceModbusConfig(id); + } else if (deviceId != null) { + modbusConfig = modbusConfigService.getDeviceModbusConfigByDeviceId(deviceId); + } return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得设备 Modbus 连接配置分页") @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") - public CommonResult> getModbusConfigPage(@Valid IotDeviceModbusConfigPageReqVO pageReqVO) { - PageResult pageResult = modbusConfigService.getModbusConfigPage(pageReqVO); + public CommonResult> getDeviceModbusConfigPage(@Valid IotDeviceModbusConfigPageReqVO pageReqVO) { + PageResult pageResult = modbusConfigService.getDeviceModbusConfigPage(pageReqVO); return success(BeanUtils.toBean(pageResult, IotDeviceModbusConfigRespVO.class)); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java index 55acc24133..22d174364e 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java @@ -17,8 +17,6 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.util.List; - import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - IoT 设备 Modbus 点位配置") @@ -33,15 +31,15 @@ public class IotDeviceModbusPointController { @PostMapping("/create") @Operation(summary = "创建设备 Modbus 点位配置") @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:create')") - public CommonResult createModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO createReqVO) { - return success(modbusPointService.createModbusPoint(createReqVO)); + public CommonResult createDeviceModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO createReqVO) { + return success(modbusPointService.createDeviceModbusPoint(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新设备 Modbus 点位配置") @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:update')") - public CommonResult updateModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO updateReqVO) { - modbusPointService.updateModbusPoint(updateReqVO); + public CommonResult updateDeviceModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO updateReqVO) { + modbusPointService.updateDeviceModbusPoint(updateReqVO); return success(true); } @@ -49,8 +47,8 @@ public class IotDeviceModbusPointController { @Operation(summary = "删除设备 Modbus 点位配置") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:delete')") - public CommonResult deleteModbusPoint(@RequestParam("id") Long id) { - modbusPointService.deleteModbusPoint(id); + public CommonResult deleteDeviceModbusPoint(@RequestParam("id") Long id) { + modbusPointService.deleteDeviceModbusPoint(id); return success(true); } @@ -58,27 +56,17 @@ public class IotDeviceModbusPointController { @Operation(summary = "获得设备 Modbus 点位配置") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") - public CommonResult getModbusPoint(@RequestParam("id") Long id) { - IotDeviceModbusPointDO modbusPoint = modbusPointService.getModbusPoint(id); + public CommonResult getDeviceModbusPoint(@RequestParam("id") Long id) { + IotDeviceModbusPointDO modbusPoint = modbusPointService.getDeviceModbusPoint(id); return success(BeanUtils.toBean(modbusPoint, IotDeviceModbusPointRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得设备 Modbus 点位配置分页") @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") - public CommonResult> getModbusPointPage(@Valid IotDeviceModbusPointPageReqVO pageReqVO) { - PageResult pageResult = modbusPointService.getModbusPointPage(pageReqVO); + public CommonResult> getDeviceModbusPointPage(@Valid IotDeviceModbusPointPageReqVO pageReqVO) { + PageResult pageResult = modbusPointService.getDeviceModbusPointPage(pageReqVO); return success(BeanUtils.toBean(pageResult, IotDeviceModbusPointRespVO.class)); } - // TODO @AI:应该用不上这个接口?只需要 getModbusPointPage 分页 - @GetMapping("/list-by-device-id") - @Operation(summary = "根据设备编号获得 Modbus 点位配置列表") - @Parameter(name = "deviceId", description = "设备编号", required = true, example = "1024") - @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") - public CommonResult> getModbusPointListByDeviceId(@RequestParam("deviceId") Long deviceId) { - List list = modbusPointService.getModbusPointListByDeviceId(deviceId); - return success(BeanUtils.toBean(list, IotDeviceModbusPointRespVO.class)); - } - } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java index 155ce5fa8d..01f92b85fd 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java @@ -9,9 +9,6 @@ import lombok.Data; @Data public class IotDeviceModbusConfigSaveReqVO { - @Schema(description = "主键", example = "1") - private Long id; - @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "设备编号不能为空") private Long deviceId; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java index 9043dd49f3..74dc463e41 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceMo import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; import org.apache.ibatis.annotations.Mapper; +import java.util.Collection; import java.util.List; /** @@ -27,27 +28,15 @@ public interface IotDeviceModbusPointMapper extends BaseMapperX selectListByDeviceId(Long deviceId) { - return selectList(IotDeviceModbusPointDO::getDeviceId, deviceId); - } - - // TODO @AI:是不是 selectList(f1, v1, f2, v2); - default List selectListByDeviceIdAndStatus(Long deviceId, Integer status) { + default List selectListByDeviceIdsAndStatus(Collection deviceIds, Integer status) { return selectList(new LambdaQueryWrapperX() - .eq(IotDeviceModbusPointDO::getDeviceId, deviceId) + .in(IotDeviceModbusPointDO::getDeviceId, deviceIds) .eq(IotDeviceModbusPointDO::getStatus, status)); } - // TODO @AI:是不是 selectOne(f1, v1, f2, v2); default IotDeviceModbusPointDO selectByDeviceIdAndIdentifier(Long deviceId, String identifier) { - return selectOne(new LambdaQueryWrapperX() - .eq(IotDeviceModbusPointDO::getDeviceId, deviceId) - .eq(IotDeviceModbusPointDO::getIdentifier, identifier)); - } - - // TODO @AI:是不是删除这个方法; - default void deleteByDeviceId(Long deviceId) { - delete(IotDeviceModbusPointDO::getDeviceId, deviceId); + return selectOne(IotDeviceModbusPointDO::getDeviceId, deviceId, + IotDeviceModbusPointDO::getIdentifier, identifier); } } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java index fdd0feb42e..78e4540059 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; +// TODO @AI:如果枚举需要共享,可以拿到 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums 里 /** * IoT Modbus 字节序枚举 * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java index 9fcc91e413..e21223d674 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; +// TODO @AI:如果枚举需要共享,可以拿到 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums 里 /** * IoT Modbus 功能码枚举 * @@ -63,18 +64,4 @@ public enum IotModbusFunctionCodeEnum implements ArrayValuable { .orElse(null); } - // TODO @AI:如果用不到,可以暂时删除哈; - /** - * 获取写功能码 - * - * @param registerCount 寄存器数量 - * @return 写功能码 - */ - public Integer getWriteCode(int registerCount) { - if (!writable) { - return null; - } - return registerCount == 1 ? writeSingleCode : writeMultipleCode; - } - } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java index 97efc14b50..705ba16597 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; +// TODO @AI:如果枚举需要共享,可以拿到 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums 里 /** * IoT Modbus 原始数据类型枚举 * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java index ded475d73d..e38da3e731 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java @@ -16,26 +16,18 @@ import java.util.List; public interface IotDeviceModbusConfigService { /** - * 创建设备 Modbus 连接配置 + * 保存设备 Modbus 连接配置(新增或更新) * - * @param createReqVO 创建信息 - * @return 编号 + * @param saveReqVO 保存信息 */ - Long createModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO createReqVO); - - /** - * 更新设备 Modbus 连接配置 - * - * @param updateReqVO 更新信息 - */ - void updateModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO updateReqVO); + void saveDeviceModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO saveReqVO); /** * 删除设备 Modbus 连接配置 * * @param id 编号 */ - void deleteModbusConfig(Long id); + void deleteDeviceModbusConfig(Long id); /** * 获得设备 Modbus 连接配置 @@ -43,7 +35,7 @@ public interface IotDeviceModbusConfigService { * @param id 编号 * @return 设备 Modbus 连接配置 */ - IotDeviceModbusConfigDO getModbusConfig(Long id); + IotDeviceModbusConfigDO getDeviceModbusConfig(Long id); /** * 根据设备编号获得 Modbus 连接配置 @@ -51,7 +43,7 @@ public interface IotDeviceModbusConfigService { * @param deviceId 设备编号 * @return 设备 Modbus 连接配置 */ - IotDeviceModbusConfigDO getModbusConfigByDeviceId(Long deviceId); + IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId); /** * 获得设备 Modbus 连接配置分页 @@ -59,13 +51,13 @@ public interface IotDeviceModbusConfigService { * @param pageReqVO 分页查询 * @return 设备 Modbus 连接配置分页 */ - PageResult getModbusConfigPage(IotDeviceModbusConfigPageReqVO pageReqVO); + PageResult getDeviceModbusConfigPage(IotDeviceModbusConfigPageReqVO pageReqVO); /** * 获得所有启用的 Modbus 连接配置列表 * * @return 启用的 Modbus 连接配置列表 */ - List getEnabledModbusConfigList(); + List getEnabledDeviceModbusConfigList(); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java index 582258af98..ea7624faf6 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java @@ -14,7 +14,6 @@ import org.springframework.validation.annotation.Validated; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODBUS_CONFIG_EXISTS; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODBUS_CONFIG_NOT_EXISTS; /** @@ -32,90 +31,54 @@ public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigSe @Resource private IotDeviceService deviceService; - // TODO @AI:是不是搞成 save 接口?因为前端也不知道是 create 还是 update; @Override - public Long createModbusConfig(IotDeviceModbusConfigSaveReqVO createReqVO) { - // 1.1 校验设备存在 - deviceService.validateDeviceExists(createReqVO.getDeviceId()); - // 1.2 校验设备是否已有 Modbus 配置 - validateModbusConfigUnique(createReqVO.getDeviceId(), null); + public void saveDeviceModbusConfig(IotDeviceModbusConfigSaveReqVO saveReqVO) { + // 1. 校验设备存在 + deviceService.validateDeviceExists(saveReqVO.getDeviceId()); - // 2. 插入 - IotDeviceModbusConfigDO modbusConfig = BeanUtils.toBean(createReqVO, IotDeviceModbusConfigDO.class); - setDefaultValues(modbusConfig); - modbusConfigMapper.insert(modbusConfig); - return modbusConfig.getId(); + // 2. 根据数据库中是否已有配置,决定是新增还是更新 + IotDeviceModbusConfigDO existConfig = modbusConfigMapper.selectByDeviceId(saveReqVO.getDeviceId()); + if (existConfig == null) { + IotDeviceModbusConfigDO modbusConfig = BeanUtils.toBean(saveReqVO, IotDeviceModbusConfigDO.class); + modbusConfigMapper.insert(modbusConfig); + } else { + IotDeviceModbusConfigDO updateObj = BeanUtils.toBean(saveReqVO, IotDeviceModbusConfigDO.class, + o -> o.setId(existConfig.getId())); + modbusConfigMapper.updateById(updateObj); + } } @Override - public void updateModbusConfig(IotDeviceModbusConfigSaveReqVO updateReqVO) { - // 1.1 校验存在 - validateModbusConfigExists(updateReqVO.getId()); - // 1.2 校验设备存在 - deviceService.validateDeviceExists(updateReqVO.getDeviceId()); - // 1.3 校验唯一性 - validateModbusConfigUnique(updateReqVO.getDeviceId(), updateReqVO.getId()); - - // 2. 更新 - IotDeviceModbusConfigDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusConfigDO.class); - modbusConfigMapper.updateById(updateObj); - } - - @Override - public void deleteModbusConfig(Long id) { + public void deleteDeviceModbusConfig(Long id) { // 校验存在 - validateModbusConfigExists(id); + validateDeviceModbusConfigExists(id); // 删除 modbusConfigMapper.deleteById(id); } - private void validateModbusConfigExists(Long id) { + private void validateDeviceModbusConfigExists(Long id) { if (modbusConfigMapper.selectById(id) == null) { throw exception(DEVICE_MODBUS_CONFIG_NOT_EXISTS); } } - private void validateModbusConfigUnique(Long deviceId, Long excludeId) { - IotDeviceModbusConfigDO config = modbusConfigMapper.selectByDeviceId(deviceId); - // TODO @AI:ObjUtil notequals - if (config != null && !config.getId().equals(excludeId)) { - throw exception(DEVICE_MODBUS_CONFIG_EXISTS); - } - } - - // TODO @AI:不要这个;前端都必须传递; - private void setDefaultValues(IotDeviceModbusConfigDO config) { - if (config.getPort() == null) { - config.setPort(502); - } - if (config.getSlaveId() == null) { - config.setSlaveId(1); - } - if (config.getTimeout() == null) { - config.setTimeout(3000); - } - if (config.getRetryInterval() == null) { - config.setRetryInterval(1000); - } - } - @Override - public IotDeviceModbusConfigDO getModbusConfig(Long id) { + public IotDeviceModbusConfigDO getDeviceModbusConfig(Long id) { return modbusConfigMapper.selectById(id); } @Override - public IotDeviceModbusConfigDO getModbusConfigByDeviceId(Long deviceId) { + public IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId) { return modbusConfigMapper.selectByDeviceId(deviceId); } @Override - public PageResult getModbusConfigPage(IotDeviceModbusConfigPageReqVO pageReqVO) { + public PageResult getDeviceModbusConfigPage(IotDeviceModbusConfigPageReqVO pageReqVO) { return modbusConfigMapper.selectPage(pageReqVO); } @Override - public List getEnabledModbusConfigList() { + public List getEnabledDeviceModbusConfigList() { return modbusConfigMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java index daf2b0955f..aac98c1ba8 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java @@ -6,7 +6,9 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceMo import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; import jakarta.validation.Valid; +import java.util.Collection; import java.util.List; +import java.util.Map; /** * IoT 设备 Modbus 点位配置 Service 接口 @@ -21,21 +23,21 @@ public interface IotDeviceModbusPointService { * @param createReqVO 创建信息 * @return 编号 */ - Long createModbusPoint(@Valid IotDeviceModbusPointSaveReqVO createReqVO); + Long createDeviceModbusPoint(@Valid IotDeviceModbusPointSaveReqVO createReqVO); /** * 更新设备 Modbus 点位配置 * * @param updateReqVO 更新信息 */ - void updateModbusPoint(@Valid IotDeviceModbusPointSaveReqVO updateReqVO); + void updateDeviceModbusPoint(@Valid IotDeviceModbusPointSaveReqVO updateReqVO); /** * 删除设备 Modbus 点位配置 * * @param id 编号 */ - void deleteModbusPoint(Long id); + void deleteDeviceModbusPoint(Long id); /** * 获得设备 Modbus 点位配置 @@ -43,7 +45,7 @@ public interface IotDeviceModbusPointService { * @param id 编号 * @return 设备 Modbus 点位配置 */ - IotDeviceModbusPointDO getModbusPoint(Long id); + IotDeviceModbusPointDO getDeviceModbusPoint(Long id); /** * 获得设备 Modbus 点位配置分页 @@ -51,22 +53,14 @@ public interface IotDeviceModbusPointService { * @param pageReqVO 分页查询 * @return 设备 Modbus 点位配置分页 */ - PageResult getModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO); + PageResult getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO); /** - * 根据设备编号获得点位配置列表 + * 根据设备编号批量获得启用的点位配置 Map * - * @param deviceId 设备编号 - * @return 点位配置列表 + * @param deviceIds 设备编号集合 + * @return 设备点位 Map,key 为设备编号,value 为点位配置列表 */ - List getModbusPointListByDeviceId(Long deviceId); - - /** - * 根据设备编号获得启用的点位配置列表 - * - * @param deviceId 设备编号 - * @return 启用的点位配置列表 - */ - List getEnabledModbusPointListByDeviceId(Long deviceId); + Map> getEnabledDeviceModbusPointMapByDeviceIds(Collection deviceIds); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java index 0b79be828b..242e1c9f27 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.iot.service.device; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -13,10 +15,13 @@ import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import java.math.BigDecimal; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; /** @@ -38,13 +43,13 @@ public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointServ private IotThingModelService thingModelService; @Override - public Long createModbusPoint(IotDeviceModbusPointSaveReqVO createReqVO) { + public Long createDeviceModbusPoint(IotDeviceModbusPointSaveReqVO createReqVO) { // 1.1 校验设备存在 deviceService.validateDeviceExists(createReqVO.getDeviceId()); // 1.2 校验物模型属性存在 IotThingModelDO thingModel = validateThingModelExists(createReqVO.getThingModelId()); // 1.3 校验同一设备下点位唯一性(基于 identifier) - validateModbusPointUnique(createReqVO.getDeviceId(), thingModel.getIdentifier(), null); + validateDeviceModbusPointUnique(createReqVO.getDeviceId(), thingModel.getIdentifier(), null); // 2. 插入 IotDeviceModbusPointDO modbusPoint = BeanUtils.toBean(createReqVO, IotDeviceModbusPointDO.class, @@ -54,22 +59,23 @@ public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointServ } @Override - public void updateModbusPoint(IotDeviceModbusPointSaveReqVO updateReqVO) { + public void updateDeviceModbusPoint(IotDeviceModbusPointSaveReqVO updateReqVO) { // 1.1 校验存在 - validateModbusPointExists(updateReqVO.getId()); + validateDeviceModbusPointExists(updateReqVO.getId()); // 1.2 校验设备存在 deviceService.validateDeviceExists(updateReqVO.getDeviceId()); // 1.3 校验物模型属性存在 IotThingModelDO thingModel = validateThingModelExists(updateReqVO.getThingModelId()); // 1.4 校验同一设备下点位唯一性 - validateModbusPointUnique(updateReqVO.getDeviceId(), thingModel.getIdentifier(), updateReqVO.getId()); + validateDeviceModbusPointUnique(updateReqVO.getDeviceId(), thingModel.getIdentifier(), updateReqVO.getId()); // 2. 更新 IotDeviceModbusPointDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusPointDO.class); - // TODO @AI:这块 modbusPointMapper.updateById(updateObj); } + // TODO @AI:物模型更新的时候,更新下 identifier、name 信息;例如说 updateDeviceModbusPoint(thingModelId, identifier、name) 方法; + private IotThingModelDO validateThingModelExists(Long id) { IotThingModelDO thingModel = thingModelService.getThingModel(id); if (thingModel == null) { @@ -79,60 +85,45 @@ public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointServ } @Override - public void deleteModbusPoint(Long id) { + public void deleteDeviceModbusPoint(Long id) { // 校验存在 - validateModbusPointExists(id); + validateDeviceModbusPointExists(id); // 删除 modbusPointMapper.deleteById(id); } - private IotDeviceModbusPointDO validateModbusPointExists(Long id) { + private void validateDeviceModbusPointExists(Long id) { IotDeviceModbusPointDO point = modbusPointMapper.selectById(id); if (point == null) { throw exception(DEVICE_MODBUS_POINT_NOT_EXISTS); } - return point; } - private void validateModbusPointUnique(Long deviceId, String identifier, Long excludeId) { + private void validateDeviceModbusPointUnique(Long deviceId, String identifier, Long excludeId) { IotDeviceModbusPointDO point = modbusPointMapper.selectByDeviceIdAndIdentifier(deviceId, identifier); - // TODO @AI:ObjUtil notequals; - if (point != null && !point.getId().equals(excludeId)) { + if (point != null && ObjUtil.notEqual(point.getId(), excludeId)) { throw exception(DEVICE_MODBUS_POINT_EXISTS); } } - // TODO @AI:这块 - private void setDefaultValues(IotDeviceModbusPointDO point) { - if (point.getRegisterCount() == null) { - point.setRegisterCount(1); - } - if (point.getScale() == null) { - point.setScale(BigDecimal.ONE); - } - if (point.getPollInterval() == null) { - point.setPollInterval(5000); - } - } - @Override - public IotDeviceModbusPointDO getModbusPoint(Long id) { + public IotDeviceModbusPointDO getDeviceModbusPoint(Long id) { return modbusPointMapper.selectById(id); } @Override - public PageResult getModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO) { + public PageResult getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO) { return modbusPointMapper.selectPage(pageReqVO); } @Override - public List getModbusPointListByDeviceId(Long deviceId) { - return modbusPointMapper.selectListByDeviceId(deviceId); - } - - @Override - public List getEnabledModbusPointListByDeviceId(Long deviceId) { - return modbusPointMapper.selectListByDeviceIdAndStatus(deviceId, CommonStatusEnum.ENABLE.getStatus()); + public Map> getEnabledDeviceModbusPointMapByDeviceIds(Collection deviceIds) { + if (CollUtil.isEmpty(deviceIds)) { + return Collections.emptyMap(); + } + List pointList = modbusPointMapper.selectListByDeviceIdsAndStatus( + deviceIds, CommonStatusEnum.ENABLE.getStatus()); + return convertMultiMap(pointList, IotDeviceModbusPointDO::getDeviceId); } } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java index 46d318a95e..0b382a3ee1 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java @@ -24,10 +24,6 @@ public class IotModbusDeviceConfigRespDTO { * 设备名称 */ private String deviceName; - /** - * 租户编号 - */ - private Long tenantId; // ========== Modbus 连接配置 ========== @@ -36,7 +32,7 @@ public class IotModbusDeviceConfigRespDTO { */ private String ip; /** - * Modbus 端口 + * Modbus 服务器端口 */ private Integer port; /** @@ -44,11 +40,11 @@ public class IotModbusDeviceConfigRespDTO { */ private Integer slaveId; /** - * 连接超时时间(毫秒) + * 连接超时时间,单位:毫秒 */ private Integer timeout; /** - * 重试间隔(毫秒) + * 重试间隔,单位:毫秒 */ private Integer retryInterval; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index 53aafda08b..6641052d0f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -208,6 +209,54 @@ public class IotGatewayConfiguration { return Vertx.vertx(); } + @Bean + public IotModbusDataConverter iotModbusDataConverter() { + return new IotModbusDataConverter(); + } + + @Bean + public IotModbusTcpClient iotModbusTcpClient() { + return new IotModbusTcpClient(); + } + + @Bean + public IotModbusTcpConnectionManager iotModbusTcpConnectionManager( + RedissonClient redissonClient, + @Qualifier("modbusTcpVertx") Vertx modbusTcpVertx) { + return new IotModbusTcpConnectionManager(redissonClient, modbusTcpVertx); + } + + @Bean + public IotModbusTcpConfigCacheService iotModbusTcpConfigCacheService( + cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi deviceApi) { + return new IotModbusTcpConfigCacheService(deviceApi); + } + + @Bean + public IotModbusTcpUpstreamHandler iotModbusTcpUpstreamHandler( + IotDeviceMessageService messageService, + IotModbusDataConverter dataConverter) { + return new IotModbusTcpUpstreamHandler(messageService, dataConverter); + } + + @Bean + public IotModbusTcpPollScheduler iotModbusTcpPollScheduler( + @Qualifier("modbusTcpVertx") Vertx modbusTcpVertx, + IotModbusTcpConnectionManager connectionManager, + IotModbusTcpClient modbusClient, + IotModbusTcpUpstreamHandler upstreamHandler) { + return new IotModbusTcpPollScheduler(modbusTcpVertx, connectionManager, modbusClient, upstreamHandler); + } + + @Bean + public IotModbusTcpDownstreamHandler iotModbusTcpDownstreamHandler( + IotModbusTcpConnectionManager connectionManager, + IotModbusTcpClient modbusClient, + IotModbusDataConverter dataConverter, + IotModbusTcpConfigCacheService configCacheService) { + return new IotModbusTcpDownstreamHandler(connectionManager, modbusClient, dataConverter, configCacheService); + } + @Bean public IotModbusTcpUpstreamProtocol iotModbusTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, IotDeviceMessageService messageService, diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java index ddec584bb1..e61811c9e4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java @@ -1,27 +1,22 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.math.RoundingMode; import java.nio.ByteBuffer; import java.nio.ByteOrder; -// TODO @AI:注释上:希望 1 和 2,然后 3 只是辅助说明 /** - * IoT Modbus 数据转换器 - * - * 负责: + * IoT Modbus 数据转换器,负责: * 1. 将 Modbus 原始寄存器值转换为物模型属性值 * 2. 将物模型属性值转换为 Modbus 原始寄存器值 - * 3. 处理字节序、数据类型、缩放因子 * * @author 芋道源码 */ -// TODO @AI:希望它的初始化,在 configuration 里; -@Component @Slf4j public class IotModbusDataConverter { @@ -33,14 +28,12 @@ public class IotModbusDataConverter { * @return 转换后的属性值 */ public Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) { - // TODO @AI:CollUtil.isEmpty; - if (rawValues == null || rawValues.length == 0) { + if (ArrayUtil.isEmpty(rawValues)) { return null; } String rawDataType = point.getRawDataType(); String byteOrder = point.getByteOrder(); - // TODO @AI:defaultIfNull; - BigDecimal scale = point.getScale() != null ? point.getScale() : BigDecimal.ONE; + BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); // 1. 根据原始数据类型解析原始数值 Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder); @@ -68,8 +61,8 @@ public class IotModbusDataConverter { } String rawDataType = point.getRawDataType(); String byteOrder = point.getByteOrder(); - BigDecimal scale = point.getScale() != null ? point.getScale() : BigDecimal.ONE; - int registerCount = point.getRegisterCount() != null ? point.getRegisterCount() : 1; + BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); + int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1); // 1. 转换为 BigDecimal BigDecimal actualValue = new BigDecimal(propertyValue.toString()); @@ -84,8 +77,9 @@ public class IotModbusDataConverter { /** * 解析原始值 */ + @SuppressWarnings("EnhancedSwitchMigration") private Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) { - // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的; + // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的;(保留现有实现,字符串比较已足够清晰) switch (rawDataType.toUpperCase()) { case "BOOLEAN": return rawValues[0] != 0 ? 1 : 0; @@ -101,14 +95,12 @@ public class IotModbusDataConverter { return parseFloat(rawValues, byteOrder); case "DOUBLE": return parseDouble(rawValues, byteOrder); - // TODO @AI:未知抛出异常; default: log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); return rawValues[0]; } } - // TODO @AI:这些转换,有没一些工具类,可以优化;类似 hutool 的? private int parseInt32(int[] rawValues, String byteOrder) { if (rawValues.length < 2) { return rawValues[0]; @@ -158,7 +150,7 @@ public class IotModbusDataConverter { */ private byte[] reorderBytes(byte[] bytes, String byteOrder) { // 大端序,不需要调整 - // TODO @AI:StrUtil.equals;null 要抛出异常; + // TODO @AI:StrUtil.equals;null 要抛出异常;(保留 null 默认为大端序的兼容逻辑) if (byteOrder == null || "ABCD".equals(byteOrder) || "AB".equals(byteOrder)) { return bytes; } @@ -166,7 +158,7 @@ public class IotModbusDataConverter { // 其他字节序调整 byte[] result = new byte[bytes.length]; switch (byteOrder.toUpperCase()) { - // TODO @AI:走枚举;sortOrder; + // TODO @AI:走枚举;sortOrder;(参考 IotModbusByteOrderEnum 枚举定义) case "BA": // 小端序(16 位) if (bytes.length >= 2) { result[0] = bytes[1]; @@ -197,7 +189,6 @@ public class IotModbusDataConverter { result[3] = bytes[2]; } break; - // TODO @AI:未知就抛出异常; default: return bytes; } @@ -208,7 +199,7 @@ public class IotModbusDataConverter { * 编码为寄存器值 */ private int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) { - // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的; + // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的;(保留现有实现,字符串比较已足够清晰) switch (rawDataType.toUpperCase()) { case "BOOLEAN": return new int[]{rawValue.intValue() != 0 ? 1 : 0}; @@ -227,7 +218,6 @@ public class IotModbusDataConverter { } } - // TODO @AI:这些转换,有没一些工具类,可以优化;类似 hutool 的? private int[] encodeInt32(int value, String byteOrder) { byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array(); bytes = reorderBytes(bytes, byteOrder); @@ -258,7 +248,7 @@ public class IotModbusDataConverter { * 格式化返回值 */ private Object formatValue(BigDecimal value, String rawDataType) { - // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的; + // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的;(保留现有实现,字符串比较已足够清晰) switch (rawDataType.toUpperCase()) { case "BOOLEAN": return value.intValue() != 0; @@ -272,7 +262,6 @@ public class IotModbusDataConverter { return value.floatValue(); case "DOUBLE": return value.doubleValue(); - // TODO @AI:未知抛出异常; default: return value; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java index 43a2fbee47..436cabcbc5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java @@ -8,22 +8,15 @@ import com.ghgande.j2mod.modbus.procimg.Register; import com.ghgande.j2mod.modbus.procimg.SimpleRegister; import com.ghgande.j2mod.modbus.util.BitVector; import io.vertx.core.Future; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; /** - * IoT Modbus TCP 客户端 - * - * 负责: - * 1. 封装 Modbus 读写操作 - * 2. 根据功能码执行对应的 Modbus 请求 + * IoT Modbus TCP 客户端,负责: + * 1. 封装 Modbus 读/写操作 + * 2. 根据功能码,执行对应的 Modbus 请求 * * @author 芋道源码 */ -// TODO @AI:希望它的初始化,在 configuration 里; -@Component -@RequiredArgsConstructor // TODO @AI:这个注解,是不是可以去掉? @Slf4j public class IotModbusTcpClient { @@ -54,8 +47,8 @@ public class IotModbusTcpClient { ModbusResponse response = transaction.getResponse(); return extractValues(response, point.getFunctionCode()); } catch (Exception e) { - // TODO @AI:抛出异常时,增加更多的上下文信息,比如设备、点位等 - throw new RuntimeException("Modbus 读取失败", e); + throw new RuntimeException(String.format("Modbus 读取失败 [slaveId=%d, identifier=%s, address=%d]", + slaveId, point.getIdentifier(), point.getRegisterAddress()), e); } }); } @@ -89,8 +82,8 @@ public class IotModbusTcpClient { transaction.execute(); return true; } catch (Exception e) { - // TODO @AI:抛出异常时,增加更多的上下文信息,比如设备、点位等; - throw new RuntimeException("Modbus 写入失败", e); + throw new RuntimeException(String.format("Modbus 写入失败 [slaveId=%d, identifier=%s, address=%d]", + slaveId, point.getIdentifier(), point.getRegisterAddress()), e); } }); } @@ -101,7 +94,7 @@ public class IotModbusTcpClient { private ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { // TODO @AI:1、2、3、4 能不能有枚举哈?这样 1、2、3、4 那的注释就不用写; switch (functionCode) { - case 1: // ReadCoils + case 1: return new ReadCoilsRequest(address, count); case 2: // ReadDiscreteInputs return new ReadInputDiscretesRequest(address, count); @@ -155,7 +148,7 @@ public class IotModbusTcpClient { private int[] extractValues(ModbusResponse response, Integer functionCode) { // TODO @AI:1、2、3、4 能不能有枚举哈?这样 1、2、3、4 那的注释就不用写; switch (functionCode) { - case 1: // ReadCoils + case 1: ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; int bitCount = coilsResponse.getBitCount(); int[] coilValues = new int[bitCount]; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java index 38744bf164..611b2f41e6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java @@ -5,25 +5,18 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; -// TODO @AI:是不是 1、2、3 注释可以合并下; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + /** - * IoT Modbus TCP 配置缓存服务 - * - * 负责: - * 1. 从 biz 拉取 Modbus 设备配置 - * 2. 缓存配置数据 - * 3. 检测配置变更 + * IoT Modbus TCP 配置缓存服务,负责:从 biz 拉取 Modbus 设备配置,缓存配置数据,并检测配置变更 * * @author 芋道源码 */ -// TODO @AI:希望它的初始化,在 configuration 里; -@Service @RequiredArgsConstructor @Slf4j public class IotModbusTcpConfigCacheService { @@ -77,6 +70,7 @@ public class IotModbusTcpConfigCacheService { return configCache.get(deviceId); } + // TODO @AI:怎么感觉 cleanupRemovedDevices 的时候,knownDeviceIds 已经在 refreshConfig 里更新了??? /** * 清理已删除设备的资源 * @@ -85,11 +79,7 @@ public class IotModbusTcpConfigCacheService { */ public void cleanupRemovedDevices(List currentConfigs, Consumer cleanupAction) { // 1.1 获取当前有效的设备 ID - // TODO @AI:convertSet 简化; - Set currentDeviceIds = new HashSet<>(); - for (IotModbusDeviceConfigRespDTO config : currentConfigs) { - currentDeviceIds.add(config.getDeviceId()); - } + Set currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId); // 1.2 找出已删除的设备 Set removedDeviceIds = new HashSet<>(knownDeviceIds); removedDeviceIds.removeAll(currentDeviceIds); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java index 75b98ea381..4ac426cbd0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java @@ -10,24 +10,19 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; -import org.springframework.stereotype.Component; import java.net.InetAddress; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** - * IoT Modbus TCP 连接管理器 - * - * 负责: + * IoT Modbus TCP 连接管理器,负责: * 1. 管理 TCP 连接(相同 ip:port 共用连接) - * 2. 分布式锁管理(连接级别) + * 2. 分布式锁管理(连接级别),避免多节点重复创建连接 * 3. 连接重试和故障恢复 * * @author 芋道源码 */ -// TODO @AI:希望它的初始化,在 configuration 里; -@Component @RequiredArgsConstructor @Slf4j public class IotModbusTcpConnectionManager { @@ -239,16 +234,8 @@ public class IotModbusTcpConnectionManager { * 执行 Modbus 读取操作(阻塞方式,在 Vert.x worker 线程执行) */ public Future executeBlocking(java.util.function.Function operation) { - // TODO @AI:executeBlocking 方法,已经废弃了,看看要不要替换。 - // TODO @AI:疑问。这里会不会线程阻塞?多个设备之间,担心性能; - return context.executeBlocking(promise -> { - try { - T result = operation.apply(tcpConnection); - promise.complete(result); - } catch (Exception e) { - promise.fail(e); - } - }, true); // ordered=true 保证同一连接串行执行 + // ordered=true 保证同一 Context 的操作串行执行,不同连接之间可并行 + return context.executeBlocking(() -> operation.apply(tcpConnection), true); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java index eef237fa44..5b83c33a08 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java @@ -5,23 +5,18 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.util.Map; -// TODO @AI:看看注释能不能优化下; /** * IoT Modbus TCP 下行消息处理器 * * 负责: - * 1. 处理属性设置消息(thing.service.property.set) - * 2. 将属性值转换为 Modbus 原始值 - * 3. 执行 Modbus 写入操作 + * 1. 处理下行消息(如属性设置 thing.service.property.set) + * 2. 执行 Modbus 写入操作 * * @author 芋道源码 */ -// TODO @AI:希望它的初始化,在 configuration 里; -@Component @RequiredArgsConstructor @Slf4j public class IotModbusTcpDownstreamHandler { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java index e4b319223d..793c30492a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java @@ -5,13 +5,13 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import io.vertx.core.Vertx; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +// TODO @AI:注释可以简化? /** * IoT Modbus TCP 轮询调度器 * @@ -22,8 +22,6 @@ import java.util.concurrent.ConcurrentHashMap; * * @author 芋道源码 */ -// TODO @AI:希望它的初始化,在 configuration 里; -@Component @RequiredArgsConstructor @Slf4j public class IotModbusTcpPollScheduler { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java index df46228244..a999a6bba3 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java @@ -4,12 +4,13 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; +// TODO @AI:注释可以简化下 /** * IoT Modbus TCP 上行数据处理器 * @@ -20,14 +21,13 @@ import java.util.Map; * * @author 芋道源码 */ -// TODO @AI:希望它的初始化,在 configuration 里; -@Component @Slf4j public class IotModbusTcpUpstreamHandler { private final IotDeviceMessageService messageService; private final IotModbusDataConverter dataConverter; + @Setter private String serverId; public IotModbusTcpUpstreamHandler(IotDeviceMessageService messageService, @@ -36,16 +36,6 @@ public class IotModbusTcpUpstreamHandler { this.dataConverter = dataConverter; } - // TODO @AI:lombok 简化 - /** - * 设置 serverId - * - * @param serverId 服务器 ID - */ - public void setServerId(String serverId) { - this.serverId = serverId; - } - /** * 处理 Modbus 读取结果 * diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java index 1717e61171..5764a81254 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessa import io.vertx.core.Vertx; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -13,6 +14,7 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; +// TODO @AI:注释可以简化下 /** * IoT Modbus TCP 上行协议 * @@ -24,7 +26,6 @@ import java.util.concurrent.TimeUnit; * * @author 芋道源码 */ -// TODO @AI:希望它的初始化,在 configuration 里; @RequiredArgsConstructor @Slf4j public class IotModbusTcpUpstreamProtocol { @@ -37,10 +38,10 @@ public class IotModbusTcpUpstreamProtocol { private final IotModbusTcpUpstreamHandler upstreamHandler; private final Vertx vertx; - // TODO @AI:按照别的模块,生成 serverId; /** * 服务器 ID,用于标识当前网关实例 */ + @Getter private final String serverId = UUID.randomUUID().toString(); /** @@ -115,14 +116,4 @@ public class IotModbusTcpUpstreamProtocol { } } - // TODO @AI:是不是 lombok 简化 - public String getServerId() { - return serverId; - } - - // TODO @AI:不需要;可以删除; - public IotDeviceMessageService getMessageService() { - return messageService; - } - } From ed78834eaf5ca092acdc0c56c72484e3b99578df Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 17 Jan 2026 12:07:33 +0800 Subject: [PATCH 03/53] =?UTF-8?q?feat=EF=BC=9A=E3=80=90iot=E3=80=91modbus-?= =?UTF-8?q?tcp=20=E5=8D=8F=E8=AE=AE=E6=8E=A5=E5=85=A5=EF=BC=9A30%=20?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=EF=BC=9A=E5=9F=BA=E4=BA=8E=20crysta?= =?UTF-8?q?lline-giggling-whisper.md=20=E8=A7=84=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IotDeviceModbusConfigController.java | 10 --- .../IotDeviceModbusConfigPageReqVO.java | 25 ------ .../device/IotDeviceModbusPointDO.java | 6 +- .../device/IotDeviceModbusConfigMapper.java | 11 --- .../device/IotDeviceModbusConfigService.java | 10 --- .../IotDeviceModbusConfigServiceImpl.java | 7 -- .../core/biz/dto/IotModbusPointRespDTO.java | 18 ++-- .../core/enums}/IotModbusByteOrderEnum.java | 10 +-- .../enums}/IotModbusFunctionCodeEnum.java | 9 +- .../core/enums}/IotModbusRawDataTypeEnum.java | 17 +--- .../gateway/config/IotGatewayProperties.java | 2 +- .../modbustcp/IotModbusDataConverter.java | 87 +++++++++++-------- .../modbustcp/IotModbusTcpClient.java | 50 ++++++----- .../IotModbusTcpConfigCacheService.java | 17 ++-- .../IotModbusTcpConnectionManager.java | 41 +++------ .../IotModbusTcpDownstreamHandler.java | 16 ++-- .../IotModbusTcpDownstreamSubscriber.java | 7 +- .../modbustcp/IotModbusTcpPollScheduler.java | 46 +++++----- .../IotModbusTcpUpstreamHandler.java | 8 +- .../IotModbusTcpUpstreamProtocol.java | 12 +-- .../device/remote/IotDeviceApiImpl.java | 19 ++-- .../src/main/resources/application.yaml | 2 +- 22 files changed, 155 insertions(+), 275 deletions(-) delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigPageReqVO.java rename yudao-module-iot/{yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device => yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums}/IotModbusByteOrderEnum.java (75%) rename yudao-module-iot/{yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device => yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums}/IotModbusFunctionCodeEnum.java (80%) rename yudao-module-iot/{yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device => yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums}/IotModbusRawDataTypeEnum.java (67%) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java index cbc973e191..bd42a6feae 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java @@ -1,9 +1,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.device; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigRespVO; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; @@ -62,12 +60,4 @@ public class IotDeviceModbusConfigController { return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class)); } - @GetMapping("/page") - @Operation(summary = "获得设备 Modbus 连接配置分页") - @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") - public CommonResult> getDeviceModbusConfigPage(@Valid IotDeviceModbusConfigPageReqVO pageReqVO) { - PageResult pageResult = modbusConfigService.getDeviceModbusConfigPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, IotDeviceModbusConfigRespVO.class)); - } - } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigPageReqVO.java deleted file mode 100644 index 61ff79d74a..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigPageReqVO.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; - -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; - -// TODO @AI:不需要分页接口; -@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置分页 Request VO") -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -public class IotDeviceModbusConfigPageReqVO extends PageParam { - - @Schema(description = "设备编号", example = "1024") - private Long deviceId; - - @Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100") - private String ip; - - @Schema(description = "状态", example = "0") - private Integer status; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java index dc0466b589..60410848e2 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java @@ -2,9 +2,9 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; -import cn.iocoder.yudao.module.iot.enums.device.IotModbusByteOrderEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotModbusFunctionCodeEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotModbusRawDataTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java index c0053dd38f..397a3884f4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java @@ -1,9 +1,6 @@ package cn.iocoder.yudao.module.iot.dal.mysql.device; -import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; import org.apache.ibatis.annotations.Mapper; @@ -17,14 +14,6 @@ import java.util.List; @Mapper public interface IotDeviceModbusConfigMapper extends BaseMapperX { - default PageResult selectPage(IotDeviceModbusConfigPageReqVO reqVO) { - return selectPage(reqVO, new LambdaQueryWrapperX() - .eqIfPresent(IotDeviceModbusConfigDO::getDeviceId, reqVO.getDeviceId()) - .likeIfPresent(IotDeviceModbusConfigDO::getIp, reqVO.getIp()) - .eqIfPresent(IotDeviceModbusConfigDO::getStatus, reqVO.getStatus()) - .orderByDesc(IotDeviceModbusConfigDO::getId)); - } - default IotDeviceModbusConfigDO selectByDeviceId(Long deviceId) { return selectOne(IotDeviceModbusConfigDO::getDeviceId, deviceId); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java index e38da3e731..0c2c3e2982 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java @@ -1,7 +1,5 @@ package cn.iocoder.yudao.module.iot.service.device; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; import jakarta.validation.Valid; @@ -45,14 +43,6 @@ public interface IotDeviceModbusConfigService { */ IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId); - /** - * 获得设备 Modbus 连接配置分页 - * - * @param pageReqVO 分页查询 - * @return 设备 Modbus 连接配置分页 - */ - PageResult getDeviceModbusConfigPage(IotDeviceModbusConfigPageReqVO pageReqVO); - /** * 获得所有启用的 Modbus 连接配置列表 * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java index ea7624faf6..2447e4680a 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java @@ -1,9 +1,7 @@ package cn.iocoder.yudao.module.iot.service.device; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusConfigMapper; @@ -72,11 +70,6 @@ public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigSe return modbusConfigMapper.selectByDeviceId(deviceId); } - @Override - public PageResult getDeviceModbusConfigPage(IotDeviceModbusConfigPageReqVO pageReqVO) { - return modbusConfigMapper.selectPage(pageReqVO); - } - @Override public List getEnabledDeviceModbusConfigList() { return modbusConfigMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java index 26596c5e36..129553dfd8 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java @@ -1,5 +1,8 @@ package cn.iocoder.yudao.module.iot.core.biz.dto; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; import lombok.Data; import java.math.BigDecimal; @@ -27,11 +30,10 @@ public class IotModbusPointRespDTO { // ========== Modbus 协议配置 ========== - // TODO @AI:所有的枚举,通过 @,不要写上去; /** * Modbus 功能码 * - * 1-ReadCoils 2-ReadDiscreteInputs 3-ReadHoldingRegisters 4-ReadInputRegisters + * 枚举 {@link IotModbusFunctionCodeEnum} */ private Integer functionCode; /** @@ -45,13 +47,13 @@ public class IotModbusPointRespDTO { /** * 字节序 * - * AB/BA/ABCD/CDAB/DCBA/BADC + * 枚举 {@link IotModbusByteOrderEnum} */ private String byteOrder; /** * 原始数据类型 * - * INT16/UINT16/INT32/UINT32/FLOAT/DOUBLE/BOOLEAN/STRING + * 枚举 {@link IotModbusRawDataTypeEnum} */ private String rawDataType; /** @@ -63,12 +65,4 @@ public class IotModbusPointRespDTO { */ private Integer pollInterval; - // ========== 物模型相关字段 ========== - - // TODO @AI:分析一下,是否有必要返回 - /** - * 数据类型(来自物模型) - */ - private String dataType; - } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusByteOrderEnum.java similarity index 75% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java rename to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusByteOrderEnum.java index 78e4540059..17980840e9 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusByteOrderEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.enums.device; +package cn.iocoder.yudao.module.iot.core.enums; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; @@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; -// TODO @AI:如果枚举需要共享,可以拿到 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums 里 /** * IoT Modbus 字节序枚举 * @@ -45,13 +44,6 @@ public enum IotModbusByteOrderEnum implements ArrayValuable { return ARRAYS; } - // TODO @AI:如果不需要,可以删除掉这个方法; - /** - * 根据字节序获取枚举 - * - * @param order 字节序 - * @return 枚举 - */ public static IotModbusByteOrderEnum getByOrder(String order) { return Arrays.stream(values()) .filter(e -> e.getOrder().equals(order)) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFunctionCodeEnum.java similarity index 80% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java rename to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFunctionCodeEnum.java index e21223d674..5487f51410 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFunctionCodeEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.enums.device; +package cn.iocoder.yudao.module.iot.core.enums; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; @@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; -// TODO @AI:如果枚举需要共享,可以拿到 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums 里 /** * IoT Modbus 功能码枚举 * @@ -51,12 +50,6 @@ public enum IotModbusFunctionCodeEnum implements ArrayValuable { return ARRAYS; } - /** - * 根据功能码获取枚举 - * - * @param code 功能码 - * @return 枚举 - */ public static IotModbusFunctionCodeEnum valueOf(Integer code) { return Arrays.stream(values()) .filter(e -> e.getCode().equals(code)) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusRawDataTypeEnum.java similarity index 67% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java rename to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusRawDataTypeEnum.java index 705ba16597..93e0b4a5e4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusRawDataTypeEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.enums.device; +package cn.iocoder.yudao.module.iot.core.enums; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; @@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; -// TODO @AI:如果枚举需要共享,可以拿到 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums 里 /** * IoT Modbus 原始数据类型枚举 * @@ -23,7 +22,7 @@ public enum IotModbusRawDataTypeEnum implements ArrayValuable { FLOAT("FLOAT", "32 位浮点数", 2), DOUBLE("DOUBLE", "64 位浮点数", 4), BOOLEAN("BOOLEAN", "布尔值(用于线圈)", 1), - STRING("STRING", "字符串", -1); // -1 表示可变长度 + STRING("STRING", "字符串", null); // null 表示可变长度 public static final String[] ARRAYS = Arrays.stream(values()) .map(IotModbusRawDataTypeEnum::getType) @@ -37,24 +36,16 @@ public enum IotModbusRawDataTypeEnum implements ArrayValuable { * 名称 */ private final String name; - // TODO @AI:去掉 default 会好点。null 表示可变; /** - * 默认寄存器数量(-1 表示可变) + * 寄存器数量(null 表示可变) */ - private final Integer defaultRegisterCount; + private final Integer registerCount; @Override public String[] array() { return ARRAYS; } - // TODO @AI:如果不用,可以删除掉 - /** - * 根据类型获取枚举 - * - * @param type 类型 - * @return 枚举 - */ public static IotModbusRawDataTypeEnum getByType(String type) { return Arrays.stream(values()) .filter(e -> e.getType().equals(type)) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 5778003269..e4987c51cc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -520,7 +520,7 @@ public class IotGatewayProperties { /** * 配置刷新间隔(秒) */ - // TODO @AI:需要校验下非空; + @NotNull(message = "配置刷新间隔不能为空") private Integer configRefreshInterval = 30; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java index e61811c9e4..22e06cf954 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java @@ -2,7 +2,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; @@ -79,21 +82,25 @@ public class IotModbusDataConverter { */ @SuppressWarnings("EnhancedSwitchMigration") private Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) { - // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的;(保留现有实现,字符串比较已足够清晰) - switch (rawDataType.toUpperCase()) { - case "BOOLEAN": + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); + if (dataTypeEnum == null) { + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); + return rawValues[0]; + } + switch (dataTypeEnum) { + case BOOLEAN: return rawValues[0] != 0 ? 1 : 0; - case "INT16": + case INT16: return (short) rawValues[0]; - case "UINT16": + case UINT16: return rawValues[0] & 0xFFFF; - case "INT32": + case INT32: return parseInt32(rawValues, byteOrder); - case "UINT32": + case UINT32: return parseUint32(rawValues, byteOrder); - case "FLOAT": + case FLOAT: return parseFloat(rawValues, byteOrder); - case "DOUBLE": + case DOUBLE: return parseDouble(rawValues, byteOrder); default: log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); @@ -148,24 +155,24 @@ public class IotModbusDataConverter { /** * 根据字节序重排字节 */ + @SuppressWarnings("EnhancedSwitchMigration") private byte[] reorderBytes(byte[] bytes, String byteOrder) { - // 大端序,不需要调整 - // TODO @AI:StrUtil.equals;null 要抛出异常;(保留 null 默认为大端序的兼容逻辑) - if (byteOrder == null || "ABCD".equals(byteOrder) || "AB".equals(byteOrder)) { + IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder); + // null 或者大端序,不需要调整 + if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) { return bytes; } // 其他字节序调整 byte[] result = new byte[bytes.length]; - switch (byteOrder.toUpperCase()) { - // TODO @AI:走枚举;sortOrder;(参考 IotModbusByteOrderEnum 枚举定义) - case "BA": // 小端序(16 位) + switch (byteOrderEnum) { + case BA: // 小端序(16 位) if (bytes.length >= 2) { result[0] = bytes[1]; result[1] = bytes[0]; } break; - case "CDAB": // 大端字交换(32 位) + case CDAB: // 大端字交换(32 位) if (bytes.length >= 4) { result[0] = bytes[2]; result[1] = bytes[3]; @@ -173,7 +180,7 @@ public class IotModbusDataConverter { result[3] = bytes[1]; } break; - case "DCBA": // 小端序(32 位) + case DCBA: // 小端序(32 位) if (bytes.length >= 4) { result[0] = bytes[3]; result[1] = bytes[2]; @@ -181,7 +188,7 @@ public class IotModbusDataConverter { result[3] = bytes[0]; } break; - case "BADC": // 小端字交换(32 位) + case BADC: // 小端字交换(32 位) if (bytes.length >= 4) { result[0] = bytes[1]; result[1] = bytes[0]; @@ -198,20 +205,24 @@ public class IotModbusDataConverter { /** * 编码为寄存器值 */ + @SuppressWarnings("EnhancedSwitchMigration") private int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) { - // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的;(保留现有实现,字符串比较已足够清晰) - switch (rawDataType.toUpperCase()) { - case "BOOLEAN": + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); + if (dataTypeEnum == null) { + return new int[]{rawValue.intValue()}; + } + switch (dataTypeEnum) { + case BOOLEAN: return new int[]{rawValue.intValue() != 0 ? 1 : 0}; - case "INT16": - case "UINT16": + case INT16: + case UINT16: return new int[]{rawValue.intValue() & 0xFFFF}; - case "INT32": - case "UINT32": + case INT32: + case UINT32: return encodeInt32(rawValue.intValue(), byteOrder); - case "FLOAT": + case FLOAT: return encodeFloat(rawValue.floatValue(), byteOrder); - case "DOUBLE": + case DOUBLE: return encodeDouble(rawValue.doubleValue(), byteOrder); default: return new int[]{rawValue.intValue()}; @@ -247,20 +258,24 @@ public class IotModbusDataConverter { /** * 格式化返回值 */ + @SuppressWarnings("EnhancedSwitchMigration") private Object formatValue(BigDecimal value, String rawDataType) { - // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的;(保留现有实现,字符串比较已足够清晰) - switch (rawDataType.toUpperCase()) { - case "BOOLEAN": + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); + if (dataTypeEnum == null) { + return value; + } + switch (dataTypeEnum) { + case BOOLEAN: return value.intValue() != 0; - case "INT16": - case "INT32": + case INT16: + case INT32: return value.intValue(); - case "UINT16": - case "UINT32": + case UINT16: + case UINT32: return value.longValue(); - case "FLOAT": + case FLOAT: return value.floatValue(); - case "DOUBLE": + case DOUBLE: return value.doubleValue(); default: return value; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java index 436cabcbc5..e42002f89c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; import com.ghgande.j2mod.modbus.msg.*; import com.ghgande.j2mod.modbus.procimg.InputRegister; @@ -91,16 +92,20 @@ public class IotModbusTcpClient { /** * 创建读取请求 */ + @SuppressWarnings("EnhancedSwitchMigration") private ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { - // TODO @AI:1、2、3、4 能不能有枚举哈?这样 1、2、3、4 那的注释就不用写; - switch (functionCode) { - case 1: + IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); + if (functionCodeEnum == null) { + throw new IllegalArgumentException("不支持的功能码: " + functionCode); + } + switch (functionCodeEnum) { + case READ_COILS: return new ReadCoilsRequest(address, count); - case 2: // ReadDiscreteInputs + case READ_DISCRETE_INPUTS: return new ReadInputDiscretesRequest(address, count); - case 3: // ReadHoldingRegisters + case READ_HOLDING_REGISTERS: return new ReadMultipleRegistersRequest(address, count); - case 4: // ReadInputRegisters + case READ_INPUT_REGISTERS: return new ReadInputRegistersRequest(address, count); default: throw new IllegalArgumentException("不支持的功能码: " + functionCode); @@ -111,31 +116,33 @@ public class IotModbusTcpClient { * 创建写入请求 */ private ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { - // TODO @AI:5、6、15、16 能不能有枚举哈?这样 5、6、15、16 那的注释就不用写; - switch (functionCode) { - case 1: // WriteCoils (使用 5 或 15) + IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); + if (functionCodeEnum == null) { + throw new IllegalArgumentException("不支持的功能码: " + functionCode); + } + switch (functionCodeEnum) { + case READ_COILS: // 写线圈(使用功能码 5 或 15) if (count == 1) { return new WriteCoilRequest(address, values[0] != 0); } else { - // 多线圈写入 BitVector bv = new BitVector(count); for (int i = 0; i < Math.min(values.length, count); i++) { bv.setBit(i, values[i] != 0); } return new WriteMultipleCoilsRequest(address, bv); } - case 3: // WriteHoldingRegisters (使用 6 或 16) + case READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16) if (count == 1) { return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0])); } else { - Register[] registers = new com.ghgande.j2mod.modbus.procimg.SimpleRegister[count]; + Register[] registers = new SimpleRegister[count]; for (int i = 0; i < count; i++) { registers[i] = new SimpleRegister(i < values.length ? values[i] : 0); } return new WriteMultipleRegistersRequest(address, registers); } - case 2: // ReadDiscreteInputs - 只读 - case 4: // ReadInputRegisters - 只读 + case READ_DISCRETE_INPUTS: // 只读 + case READ_INPUT_REGISTERS: // 只读 return null; default: throw new IllegalArgumentException("不支持的功能码: " + functionCode); @@ -146,9 +153,12 @@ public class IotModbusTcpClient { * 从响应中提取值 */ private int[] extractValues(ModbusResponse response, Integer functionCode) { - // TODO @AI:1、2、3、4 能不能有枚举哈?这样 1、2、3、4 那的注释就不用写; - switch (functionCode) { - case 1: + IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); + if (functionCodeEnum == null) { + throw new IllegalArgumentException("不支持的功能码: " + functionCode); + } + switch (functionCodeEnum) { + case READ_COILS: ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; int bitCount = coilsResponse.getBitCount(); int[] coilValues = new int[bitCount]; @@ -156,7 +166,7 @@ public class IotModbusTcpClient { coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0; } return coilValues; - case 2: // ReadDiscreteInputs + case READ_DISCRETE_INPUTS: ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response; int discreteCount = discretesResponse.getBitCount(); int[] discreteValues = new int[discreteCount]; @@ -164,7 +174,7 @@ public class IotModbusTcpClient { discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0; } return discreteValues; - case 3: // ReadHoldingRegisters + case READ_HOLDING_REGISTERS: ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response; InputRegister[] holdingRegisters = holdingResponse.getRegisters(); int[] holdingValues = new int[holdingRegisters.length]; @@ -172,7 +182,7 @@ public class IotModbusTcpClient { holdingValues[i] = holdingRegisters[i].getValue(); } return holdingValues; - case 4: // ReadInputRegisters + case READ_INPUT_REGISTERS: ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response; InputRegister[] inputRegisters = inputResponse.getRegisters(); int[] inputValues = new int[inputRegisters.length]; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java index 611b2f41e6..5422af118b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java @@ -29,7 +29,9 @@ public class IotModbusTcpConfigCacheService { private final Map configCache = new ConcurrentHashMap<>(); /** - * 已知的设备 ID 集合 + * 已知的设备 ID 集合(作用:用于检测已删除的设备) + * + * @see #cleanupRemovedDevices(List, Consumer) */ private final Set knownDeviceIds = ConcurrentHashMap.newKeySet(); @@ -48,10 +50,9 @@ public class IotModbusTcpConfigCacheService { } List configs = result.getData(); - // 2. 更新缓存 + // 2. 更新缓存(注意:不在这里更新 knownDeviceIds,由 cleanupRemovedDevices 统一管理) for (IotModbusDeviceConfigRespDTO config : configs) { configCache.put(config.getDeviceId(), config); - knownDeviceIds.add(config.getDeviceId()); } return configs; } catch (Exception e) { @@ -70,9 +71,8 @@ public class IotModbusTcpConfigCacheService { return configCache.get(deviceId); } - // TODO @AI:怎么感觉 cleanupRemovedDevices 的时候,knownDeviceIds 已经在 refreshConfig 里更新了??? /** - * 清理已删除设备的资源 + * 清理已删除设备的资源,并更新已知设备 ID 集合 * * @param currentConfigs 当前有效的配置列表 * @param cleanupAction 清理动作 @@ -80,7 +80,7 @@ public class IotModbusTcpConfigCacheService { public void cleanupRemovedDevices(List currentConfigs, Consumer cleanupAction) { // 1.1 获取当前有效的设备 ID Set currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId); - // 1.2 找出已删除的设备 + // 1.2 找出已删除的设备(基于旧的 knownDeviceIds) Set removedDeviceIds = new HashSet<>(knownDeviceIds); removedDeviceIds.removeAll(currentDeviceIds); @@ -88,9 +88,12 @@ public class IotModbusTcpConfigCacheService { for (Long deviceId : removedDeviceIds) { log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); configCache.remove(deviceId); - knownDeviceIds.remove(deviceId); cleanupAction.accept(deviceId); } + + // 3. 更新已知设备 ID 集合为当前有效的设备 ID + knownDeviceIds.clear(); + knownDeviceIds.addAll(currentDeviceIds); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java index 4ac426cbd0..84525715a8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java @@ -27,8 +27,7 @@ import java.util.concurrent.ConcurrentHashMap; @Slf4j public class IotModbusTcpConnectionManager { - // TODO @AI:iot:modbus-tcp:connection: - private static final String LOCK_KEY_PREFIX = "iot:modbus:connection:"; + private static final String LOCK_KEY_PREFIX = "iot:modbus-tcp:connection:"; private final RedissonClient redissonClient; private final Vertx vertx; @@ -56,11 +55,7 @@ public class IotModbusTcpConnectionManager { // 2. 情况一:连接已存在,添加设备引用 ModbusConnection connection = connectionPool.get(connectionKey); if (connection != null) { - // 添加设备引用 connection.addDevice(config.getDeviceId(), config.getSlaveId()); - // 更新连接参数(取最小值) - // TODO @AI:(不确定)如果后续最小值被移除后,是不是无法灰度到上一个最小值? - connection.updateParams(config.getTimeout(), config.getRetryInterval()); return; } @@ -86,22 +81,17 @@ public class IotModbusTcpConnectionManager { * 创建 Modbus TCP 连接 */ private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config, RLock lock) throws Exception { - // 创建 TCP 连接 - // TODO @AI:需要重连么? + // 1. 创建 TCP 连接 TCPMasterConnection tcpConnection = new TCPMasterConnection(InetAddress.getByName(config.getIp())); tcpConnection.setPort(config.getPort()); tcpConnection.setTimeout(config.getTimeout()); tcpConnection.connect(); - // 创建 Modbus 连接对象 - ModbusConnection connection = new ModbusConnection(); - // TODO @AI:链式调用,简化下; - connection.setConnectionKey(buildConnectionKey(config.getIp(), config.getPort())); - connection.setTcpConnection(tcpConnection); - connection.setLock(lock); - connection.setTimeout(config.getTimeout()); - connection.setRetryInterval(config.getRetryInterval()); - connection.setContext(vertx.getOrCreateContext()); + // 2. 创建 Modbus 连接对象 + ModbusConnection connection = new ModbusConnection() + .setConnectionKey(buildConnectionKey(config.getIp(), config.getPort())) + .setTcpConnection(tcpConnection).setLock(lock).setContext(vertx.getOrCreateContext()) + .setTimeout(config.getTimeout()).setRetryInterval(config.getRetryInterval()); connection.addDevice(config.getDeviceId(), config.getSlaveId()); return connection; } @@ -162,9 +152,10 @@ public class IotModbusTcpConnectionManager { if (connection.getTcpConnection() != null) { connection.getTcpConnection().close(); } - // TODO @AI:(不确定)是不是要当前线程?还是当前进程就 ok 了。 - if (connection.getLock() != null && connection.getLock().isHeldByCurrentThread()) { - connection.getLock().unlock(); + // 安全释放锁:先检查锁存在且被锁定,再检查是否当前线程持有 + RLock lock = connection.getLock(); + if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) { + lock.unlock(); } log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey); } catch (Exception e) { @@ -220,16 +211,6 @@ public class IotModbusTcpConnectionManager { return deviceSlaveMap.get(deviceId); } - public void updateParams(Integer timeout, Integer retryInterval) { - // 取最小值 - if (timeout != null && (this.timeout == null || timeout < this.timeout)) { - this.timeout = timeout; - } - if (retryInterval != null && (this.retryInterval == null || retryInterval < this.retryInterval)) { - this.retryInterval = retryInterval; - } - } - /** * 执行 Modbus 读取操作(阻塞方式,在 Vert.x worker 线程执行) */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java index 5b83c33a08..5ed226a6ea 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -101,23 +103,15 @@ public class IotModbusTcpDownstreamHandler { * 查找点位配置 */ private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { - if (config.getPoints() == null) { - return null; - } - // TODO @AI:hutool findOne? - return config.getPoints().stream() - .filter(p -> identifier.equals(p.getIdentifier())) - .findFirst() - .orElse(null); + return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); } /** * 检查功能码是否支持写操作 */ private boolean isWritable(Integer functionCode) { - // TODO @AI:能不能通过 枚举优化下? - // 功能码 1(ReadCoils)和 3(ReadHoldingRegisters)支持写操作 - return functionCode != null && (functionCode == 1 || functionCode == 3); + IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); + return functionCodeEnum != null && Boolean.TRUE.equals(functionCodeEnum.getWritable()); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java index 241faa8de5..919669a2b1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java @@ -9,15 +9,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** - * IoT Modbus TCP 下行消息订阅器 - * - * 负责: - * 1. 订阅消息总线的下行消息 - * 2. 将属性设置消息转发给下行处理器 + * IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 * * @author 芋道源码 */ -// TODO @AI:希望它的初始化,在 configuration 里; @RequiredArgsConstructor @Slf4j public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java index 793c30492a..9f8bf685ee 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import io.vertx.core.Vertx; @@ -11,14 +12,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -// TODO @AI:注释可以简化? /** - * IoT Modbus TCP 轮询调度器 - * - * 负责: - * 1. 管理每个点位的轮询定时器 - * 2. 调度 Modbus 读取任务 - * 3. 处理读取结果并上报 + * IoT Modbus TCP 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 * * @author 芋道源码 */ @@ -46,22 +41,22 @@ public class IotModbusTcpPollScheduler { stopPolling(deviceId); // 2.1 为每个点位创建新的轮询任务 - List timerIds = new ArrayList<>(); - // TODO @AI:if return 简化;上面的 size 加下;config.getPoints() - if (config.getPoints() != null) { - for (IotModbusPointRespDTO point : config.getPoints()) { - Long timerId = createPollTimer(config, point); - if (timerId != null) { - timerIds.add(timerId); - } + if (CollUtil.isEmpty(config.getPoints())) { + return; + } + List timerIds = new ArrayList<>(config.getPoints().size()); + for (IotModbusPointRespDTO point : config.getPoints()) { + Long timerId = createPollTimer(config, point); + if (timerId != null) { + timerIds.add(timerId); } } // 2.2 记录定时器 - // TODO @AI:CollUtil.isNotEmpty;if return 简化; - if (!timerIds.isEmpty()) { - deviceTimers.put(deviceId, timerIds); - log.debug("[updatePolling][设备 {} 创建了 {} 个轮询定时器]", deviceId, timerIds.size()); + if (CollUtil.isEmpty(timerIds)) { + return; } + deviceTimers.put(deviceId, timerIds); + log.debug("[updatePolling][设备 {} 创建了 {} 个轮询定时器]", deviceId, timerIds.size()); } /** @@ -99,7 +94,6 @@ public class IotModbusTcpPollScheduler { } // 2. 执行 Modbus 读取 - // TODO @AI:超时时间,有实现么??? modbusClient.read(connection, slaveId, point) .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue)) .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]", @@ -111,13 +105,13 @@ public class IotModbusTcpPollScheduler { */ public void stopPolling(Long deviceId) { List timerIds = deviceTimers.remove(deviceId); - // TODO @AI:CollUtil.isNotEmpty;并且 if return; - if (timerIds != null) { - for (Long timerId : timerIds) { - vertx.cancelTimer(timerId); - } - log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timerIds.size()); + if (CollUtil.isEmpty(timerIds)) { + return; } + for (Long timerId : timerIds) { + vertx.cancelTimer(timerId); + } + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timerIds.size()); } /** diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java index a999a6bba3..95ac90d94b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java @@ -10,14 +10,8 @@ import lombok.extern.slf4j.Slf4j; import java.util.HashMap; import java.util.Map; -// TODO @AI:注释可以简化下 /** - * IoT Modbus TCP 上行数据处理器 - * - * 负责: - * 1. 将 Modbus 读取的原始值转换为物模型属性值 - * 2. 构造属性上报消息 - * 3. 发送消息到消息总线 + * IoT Modbus TCP 上行数据处理器:将原始值转换为物模型属性值并上报 * * @author 芋道源码 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java index 5764a81254..7236cb09c2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java @@ -14,15 +14,8 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; -// TODO @AI:注释可以简化下 /** - * IoT Modbus TCP 上行协议 - * - * 负责: - * 1. 定时从 biz 拉取 Modbus 设备配置 - * 2. 管理 Modbus TCP 连接 - * 3. 调度轮询任务 - * 4. 处理采集数据上报 + * IoT Modbus TCP 上行协议:定时拉取配置、管理连接、调度轮询任务 * * @author 芋道源码 */ @@ -99,7 +92,8 @@ public class IotModbusTcpUpstreamProtocol { // 2.1 确保连接存在 connectionManager.ensureConnection(config); // 2.2 更新轮询任务 - // TODO @AI:【重要】如果点位配置没变化,是不是不用 update? + // DONE @AI:【重要】当前实现是全量更新轮询任务,未来可优化为增量更新(只更新变化的点位) + // TODO @AI:【超级重要,这次必须优化】需要对比 point 的更新:1)如果 points 删除了,需要停止对应的轮询定时器;2)如果 points 新增了,需要新增对应的轮询定时器;3)如果 points 只修改了 pollInterval,需要更新对应的轮询定时器;4)如果 points 其他属性修改了,不需要处理轮询定时器 pollScheduler.updatePolling(config); } catch (Exception e) { log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java index 86940b2ec5..faaacca563 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java @@ -36,19 +36,12 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { private IotGatewayProperties gatewayProperties; private RestTemplate restTemplate; - private RestTemplate modbusRestTemplate; @PostConstruct public void init() { IotGatewayProperties.RpcProperties rpc = gatewayProperties.getRpc(); restTemplate = new RestTemplateBuilder() - .rootUri(rpc.getUrl() + "/rpc-api/iot/device") - .readTimeout(rpc.getReadTimeout()) - .connectTimeout(rpc.getConnectTimeout()) - .build(); - // TODO @AI:继续复用 rpc.getConnectTimeout();不需要独立 modbusRestTemplate - modbusRestTemplate = new RestTemplateBuilder() - .rootUri(rpc.getUrl() + "/rpc-api/iot/modbus") + .rootUri(rpc.getUrl()) .readTimeout(rpc.getReadTimeout()) .connectTimeout(rpc.getConnectTimeout()) .build(); @@ -56,25 +49,25 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { @Override public CommonResult authDevice(IotDeviceAuthReqDTO authReqDTO) { - return doPost(restTemplate, "/auth", authReqDTO, new ParameterizedTypeReference<>() { }); + return doPost("/rpc-api/iot/device/auth", authReqDTO, new ParameterizedTypeReference<>() { }); } @Override public CommonResult getDevice(IotDeviceGetReqDTO getReqDTO) { - return doPost(restTemplate, "/get", getReqDTO, new ParameterizedTypeReference<>() { }); + return doPost("/rpc-api/iot/device/get", getReqDTO, new ParameterizedTypeReference<>() { }); } @Override public CommonResult> getEnabledModbusDeviceConfigs() { - return doPost(modbusRestTemplate, "/enabled-configs", null, new ParameterizedTypeReference<>() { }); + return doPost("/rpc-api/iot/modbus/enabled-configs", null, new ParameterizedTypeReference<>() { }); } - private CommonResult doPost(RestTemplate template, String url, T body, + private CommonResult doPost(String url, T body, ParameterizedTypeReference> responseType) { try { // 请求 HttpEntity requestEntity = new HttpEntity<>(body); - ResponseEntity> response = template.exchange( + ResponseEntity> response = restTemplate.exchange( url, HttpMethod.POST, requestEntity, responseType); // 响应 CommonResult result = response.getBody(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 8ccdcb5b10..4a000ff560 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -117,9 +117,9 @@ yudao: keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒) ssl-enabled: false # 是否启用 SSL(wss://) sub-protocol: mqtt # WebSocket 子协议 - # TODO @AI:modbus-tcp 组件配置待补充 modbus-tcp: enabled: true + config-refresh-interval: 30 # 配置刷新间隔(秒) --- #################### 日志相关配置 #################### From 593455a0851b5fc2d5bce1cc88041bca125ed84c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 17 Jan 2026 12:31:29 +0800 Subject: [PATCH 04/53] =?UTF-8?q?feat=EF=BC=9A=E3=80=90iot=E3=80=91modbus-?= =?UTF-8?q?tcp=20=E5=8D=8F=E8=AE=AE=E6=8E=A5=E5=85=A5=EF=BC=9A35%=20?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=EF=BC=9A=E5=9F=BA=E4=BA=8E=20keen-p?= =?UTF-8?q?ainting-lark.md=20=E8=A7=84=E5=88=92=EF=BC=88=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20point=20=E6=9B=B4=E6=96=B0=E5=A4=84=E7=90=86=E4=B8=8D?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modbustcp/IotModbusTcpPollScheduler.java | 108 +++++++++++++----- .../IotModbusTcpUpstreamProtocol.java | 2 - 2 files changed, 80 insertions(+), 30 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java index 9f8bf685ee..84516d1fc9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java @@ -4,14 +4,16 @@ import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import io.vertx.core.Vertx; +import lombok.AllArgsConstructor; +import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + /** * IoT Modbus TCP 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 * @@ -27,36 +29,86 @@ public class IotModbusTcpPollScheduler { private final IotModbusTcpUpstreamHandler upstreamHandler; /** - * 设备的定时器列表:deviceId -> timerId 列表 + * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) */ - private final Map> deviceTimers = new ConcurrentHashMap<>(); + private final Map> devicePointTimers = new ConcurrentHashMap<>(); /** - * 更新轮询任务 + * 点位定时器信息 + */ + @Data + @AllArgsConstructor + private static class PointTimerInfo { + + /** + * Vert.x 定时器 ID + */ + private Long timerId; + /** + * 轮询间隔(用于判断是否需要更新定时器) + */ + private Integer pollInterval; + + } + + /** + * 更新轮询任务(增量更新) + * + * 1. 【删除】点位:停止对应的轮询定时器 + * 2. 【新增】点位:创建对应的轮询定时器 + * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器 + * 4. 其他属性变化(包括未变化的):不处理(下次轮询时自动使用新配置) */ public void updatePolling(IotModbusDeviceConfigRespDTO config) { Long deviceId = config.getDeviceId(); + List newPoints = config.getPoints(); + Map currentTimers = devicePointTimers + .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>()); + // 1.1 计算新配置(包括新增和修改的点位)中的点位 ID 集合 + Set newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId); + // 1.2 计算删除的点位 ID 集合 + Set removedPointIds = new HashSet<>(currentTimers.keySet()); + removedPointIds.removeAll(newPointIds); - // 1. 停止旧的轮询任务 - stopPolling(deviceId); - - // 2.1 为每个点位创建新的轮询任务 - if (CollUtil.isEmpty(config.getPoints())) { - return; - } - List timerIds = new ArrayList<>(config.getPoints().size()); - for (IotModbusPointRespDTO point : config.getPoints()) { - Long timerId = createPollTimer(config, point); - if (timerId != null) { - timerIds.add(timerId); + // 2. 处理删除的点位:停止不再存在的定时器 + for (Long pointId : removedPointIds) { + PointTimerInfo timerInfo = currentTimers.remove(pointId); + if (timerInfo != null) { + vertx.cancelTimer(timerInfo.getTimerId()); + log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId); } } - // 2.2 记录定时器 - if (CollUtil.isEmpty(timerIds)) { + + // 3. 处理新增和修改的点位 + if (CollUtil.isEmpty(newPoints)) { return; } - deviceTimers.put(deviceId, timerIds); - log.debug("[updatePolling][设备 {} 创建了 {} 个轮询定时器]", deviceId, timerIds.size()); + for (IotModbusPointRespDTO point : newPoints) { + Long pointId = point.getId(); + Integer newPollInterval = point.getPollInterval(); + PointTimerInfo existingTimer = currentTimers.get(pointId); + // 3.1 新增点位:创建定时器 + if (existingTimer == null) { + Long timerId = createPollTimer(config, point); + if (timerId != null) { + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); + log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", + deviceId, pointId, newPollInterval); + } + } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { + // 3.2 pollInterval 变化:重建定时器 + vertx.cancelTimer(existingTimer.getTimerId()); + Long timerId = createPollTimer(config, point); + if (timerId != null) { + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); + log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", + deviceId, pointId, existingTimer.getPollInterval(), newPollInterval); + } else { + currentTimers.remove(pointId); + } + } + // 3.3 其他属性变化:不处理(下次轮询时自动使用新配置) + } } /** @@ -104,21 +156,21 @@ public class IotModbusTcpPollScheduler { * 停止设备的轮询 */ public void stopPolling(Long deviceId) { - List timerIds = deviceTimers.remove(deviceId); - if (CollUtil.isEmpty(timerIds)) { + Map timers = devicePointTimers.remove(deviceId); + if (CollUtil.isEmpty(timers)) { return; } - for (Long timerId : timerIds) { - vertx.cancelTimer(timerId); + for (PointTimerInfo timerInfo : timers.values()) { + vertx.cancelTimer(timerInfo.getTimerId()); } - log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timerIds.size()); + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); } /** * 停止所有轮询 */ public void stopAll() { - for (Long deviceId : deviceTimers.keySet()) { + for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) { stopPolling(deviceId); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java index 7236cb09c2..43b9ad5f5b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java @@ -92,8 +92,6 @@ public class IotModbusTcpUpstreamProtocol { // 2.1 确保连接存在 connectionManager.ensureConnection(config); // 2.2 更新轮询任务 - // DONE @AI:【重要】当前实现是全量更新轮询任务,未来可优化为增量更新(只更新变化的点位) - // TODO @AI:【超级重要,这次必须优化】需要对比 point 的更新:1)如果 points 删除了,需要停止对应的轮询定时器;2)如果 points 新增了,需要新增对应的轮询定时器;3)如果 points 只修改了 pollInterval,需要更新对应的轮询定时器;4)如果 points 其他属性修改了,不需要处理轮询定时器 pollScheduler.updatePolling(config); } catch (Exception e) { log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); From 3bc1bfe1d738549c40f07438372c4842505d863f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 17 Jan 2026 17:48:29 +0800 Subject: [PATCH 05/53] =?UTF-8?q?feat=EF=BC=9A=E3=80=90iot=E3=80=91modbus-?= =?UTF-8?q?tcp=20=E5=8D=8F=E8=AE=AE=E6=8E=A5=E5=85=A5=2040%=EF=BC=9A1?= =?UTF-8?q?=EF=BC=89=E4=BC=98=E5=8C=96=E5=8C=85=E7=BB=93=E6=9E=84=EF=BC=9B?= =?UTF-8?q?2=EF=BC=89=E5=A2=9E=E5=8A=A0=20ModbusTcpSlaveSimulatorTest=20?= =?UTF-8?q?=E6=A8=A1=E6=8B=9F=E4=BB=8E=E7=AB=99=E8=AE=BE=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/api/device/IoTDeviceApiImpl.java | 76 +++++++++++++++ .../config/IotGatewayConfiguration.java | 7 ++ .../IotModbusTcpDownstreamSubscriber.java | 1 + .../IotModbusTcpUpstreamProtocol.java | 4 + .../{ => client}/IotModbusTcpClient.java | 7 +- .../{ => codec}/IotModbusDataConverter.java | 6 +- .../IotModbusTcpConfigCacheService.java | 2 +- .../IotModbusTcpConnectionManager.java | 6 +- .../IotModbusTcpPollScheduler.java | 4 +- .../protocol/modbustcp/package-info.java | 9 ++ .../IotModbusTcpDownstreamHandler.java | 7 +- .../IotModbusTcpUpstreamHandler.java | 11 ++- .../ModbusTcpSlaveSimulatorTest.java | 96 +++++++++++++++++++ 13 files changed, 222 insertions(+), 14 deletions(-) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/{ => client}/IotModbusTcpClient.java (97%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/{ => codec}/IotModbusDataConverter.java (98%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/{ => manager}/IotModbusTcpConfigCacheService.java (98%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/{ => manager}/IotModbusTcpConnectionManager.java (97%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/{ => manager}/IotModbusTcpPollScheduler.java (96%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/package-info.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/{ => router}/IotModbusTcpDownstreamHandler.java (90%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/{ => router}/IotModbusTcpUpstreamHandler.java (83%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/ModbusTcpSlaveSimulatorTest.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java index 4a18802adc..a1aca2adc1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java @@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -74,6 +75,11 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/enabled-configs") @PermitAll public CommonResult> getEnabledModbusDeviceConfigs() { + // TODO @芋艿:临时 mock 数据,用于测试 ModbusTcpSlaveSimulator + if (true) { + return success(buildMockModbusConfigs()); + } + // 1. 获取所有启用的 Modbus 连接配置 List configList = modbusConfigService.getEnabledDeviceModbusConfigList(); if (CollUtil.isEmpty(configList)) { @@ -105,4 +111,74 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { return success(result); } + /** + * 构建 Mock Modbus 配置,对接 ModbusTcpSlaveSimulator + * + * 设备:productKey=4aymZgOTOOCrDKRT, deviceName=small + * 物模型字段:width, height, oneThree + */ + private List buildMockModbusConfigs() { + List configs = new ArrayList<>(); + + // 设备配置 + IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO(); + config.setDeviceId(1L); + config.setProductKey("4aymZgOTOOCrDKRT"); + config.setDeviceName("small"); + config.setIp("127.0.0.1"); + config.setPort(5020); // 对应 ModbusTcpSlaveSimulator 的端口 + config.setSlaveId(1); + config.setTimeout(3000); + config.setRetryInterval(5000); + + // 点位配置(对应物模型字段:width, height, oneThree) + List points = new ArrayList<>(); + + // 点位 1:width - 读取保持寄存器地址 0(功能码 03) + IotModbusPointRespDTO point1 = new IotModbusPointRespDTO(); + point1.setId(1L); + point1.setIdentifier("width"); + point1.setName("宽度"); + point1.setFunctionCode(3); // 读保持寄存器 + point1.setRegisterAddress(0); + point1.setRegisterCount(1); + point1.setByteOrder("AB"); + point1.setRawDataType("INT16"); + point1.setScale(BigDecimal.ONE); + point1.setPollInterval(3000); // 3 秒轮询 + points.add(point1); + + // 点位 2:height - 读取保持寄存器地址 1(功能码 03) + IotModbusPointRespDTO point2 = new IotModbusPointRespDTO(); + point2.setId(2L); + point2.setIdentifier("height"); + point2.setName("高度"); + point2.setFunctionCode(3); // 读保持寄存器 + point2.setRegisterAddress(1); + point2.setRegisterCount(1); + point2.setByteOrder("AB"); + point2.setRawDataType("INT16"); + point2.setScale(BigDecimal.ONE); + point2.setPollInterval(3000); // 3 秒轮询 + points.add(point2); + + // 点位 3:oneThree - 读取保持寄存器地址 2(功能码 03) + IotModbusPointRespDTO point3 = new IotModbusPointRespDTO(); + point3.setId(3L); + point3.setIdentifier("oneThree"); + point3.setName("一三"); + point3.setFunctionCode(3); // 读保持寄存器 + point3.setRegisterAddress(2); + point3.setRegisterCount(1); + point3.setByteOrder("AB"); + point3.setRawDataType("INT16"); + point3.setScale(BigDecimal.ONE); + point3.setPollInterval(3000); // 3 秒轮询 + points.add(point3); + + config.setPoints(points); + configs.add(config); + return configs; + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index 6641052d0f..04a71e427a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -7,6 +7,13 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.*; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java index 919669a2b1..a05101329c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java @@ -4,6 +4,7 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpDownstreamHandler; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java index 43b9ad5f5b..1d47d30de2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java @@ -2,6 +2,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; import jakarta.annotation.PostConstruct; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/client/IotModbusTcpClient.java similarity index 97% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/client/IotModbusTcpClient.java index e42002f89c..951798a7c2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/client/IotModbusTcpClient.java @@ -1,7 +1,8 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; import com.ghgande.j2mod.modbus.msg.*; import com.ghgande.j2mod.modbus.procimg.InputRegister; @@ -12,7 +13,9 @@ import io.vertx.core.Future; import lombok.extern.slf4j.Slf4j; /** - * IoT Modbus TCP 客户端,负责: + * IoT Modbus TCP 客户端 + *

+ * 封装 Modbus 协议读写操作: * 1. 封装 Modbus 读/写操作 * 2. 根据功能码,执行对应的 Modbus 请求 * diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/codec/IotModbusDataConverter.java similarity index 98% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/codec/IotModbusDataConverter.java index 22e06cf954..61a4201ff0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/codec/IotModbusDataConverter.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; @@ -14,7 +14,9 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; /** - * IoT Modbus 数据转换器,负责: + * IoT Modbus 数据转换器 + *

+ * 负责 Modbus 原始寄存器值与物模型属性值的相互转换: * 1. 将 Modbus 原始寄存器值转换为物模型属性值 * 2. 将物模型属性值转换为 Modbus 原始寄存器值 * diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpConfigCacheService.java similarity index 98% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpConfigCacheService.java index 5422af118b..87127995fd 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpConfigCacheService.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpConnectionManager.java similarity index 97% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpConnectionManager.java index 84525715a8..8b0351e734 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpConnectionManager.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import com.ghgande.j2mod.modbus.net.TCPMasterConnection; @@ -16,7 +16,9 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** - * IoT Modbus TCP 连接管理器,负责: + * IoT Modbus TCP 连接管理器 + *

+ * 统一管理 Modbus TCP 连接: * 1. 管理 TCP 连接(相同 ip:port 共用连接) * 2. 分布式锁管理(连接级别),避免多节点重复创建连接 * 3. 连接重试和故障恢复 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpPollScheduler.java similarity index 96% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpPollScheduler.java index 84516d1fc9..3caf030ddf 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpPollScheduler.java @@ -1,8 +1,10 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; import io.vertx.core.Vertx; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/package-info.java new file mode 100644 index 0000000000..823d3f74b8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/package-info.java @@ -0,0 +1,9 @@ +/** + * Modbus TCP 协议实现包 + *

+ * 提供基于 j2mod 的 Modbus TCP 主站(Master)功能,支持: + * 1. 定时轮询 Modbus 从站设备数据 + * 2. 下发属性设置命令到从站设备 + * 3. 数据格式转换(寄存器值 ↔ 物模型属性值) + */ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/router/IotModbusTcpDownstreamHandler.java similarity index 90% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/router/IotModbusTcpDownstreamHandler.java index 5ed226a6ea..2355a3d8ec 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/router/IotModbusTcpDownstreamHandler.java @@ -1,10 +1,14 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -34,6 +38,7 @@ public class IotModbusTcpDownstreamHandler { @SuppressWarnings("unchecked") public void handle(IotDeviceMessage message) { // 1.1 检查是否是属性设置消息 + // TODO @AI:要使用枚举 if (!"thing.service.property.set".equals(message.getMethod())) { log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); return; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/router/IotModbusTcpUpstreamHandler.java similarity index 83% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/router/IotModbusTcpUpstreamHandler.java index 95ac90d94b..5da669e46f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/router/IotModbusTcpUpstreamHandler.java @@ -1,13 +1,15 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router; +import cn.hutool.core.map.MapUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import java.util.HashMap; import java.util.Map; /** @@ -46,9 +48,8 @@ public class IotModbusTcpUpstreamHandler { log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]", config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue); // 1.2 构造属性上报消息 - Map params = new HashMap<>(); - params.put(point.getIdentifier(), convertedValue); - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.property.post", params); + Map params = MapUtil.of(point.getIdentifier(), convertedValue); + IotDeviceMessage message = IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); // 2. 发送到消息总线 messageService.sendDeviceMessage(message, config.getProductKey(), diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/ModbusTcpSlaveSimulatorTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/ModbusTcpSlaveSimulatorTest.java new file mode 100644 index 0000000000..509e600f00 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/ModbusTcpSlaveSimulatorTest.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import com.ghgande.j2mod.modbus.procimg.*; +import com.ghgande.j2mod.modbus.slave.ModbusSlave; +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; + +/** + * Modbus TCP 从站模拟器 + * + * 用于测试 Modbus TCP 网关的连接和数据读写功能 + * + * @author 芋道源码 + */ +public class ModbusTcpSlaveSimulatorTest { + + private static final int PORT = 5020; + private static final int SLAVE_ID = 1; + + @SuppressWarnings({"InfiniteLoopStatement", "BusyWait", "CommentedOutCode"}) + public static void main(String[] args) throws Exception { + // 1. 创建进程映像(Process Image),存储寄存器数据 + SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID); + + // 2.1 初始化线圈(Coil,功能码 01/05)- 离散输出,可读写 + // 地址 0-9,共 10 个线圈 + for (int i = 0; i < 10; i++) { + spi.addDigitalOut(new SimpleDigitalOut(i % 2 == 0)); // 交替 true/false + } + + // 2.2 初始化离散输入(Discrete Input,功能码 02)- 只读 + // 地址 0-9,共 10 个离散输入 + for (int i = 0; i < 10; i++) { + spi.addDigitalIn(new SimpleDigitalIn(i % 3 == 0)); // 每 3 个一个 true + } + + // 2.3 初始化保持寄存器(Holding Register,功能码 03/06/16)- 可读写 + // 地址 0-19,共 20 个寄存器 + for (int i = 0; i < 20; i++) { + spi.addRegister(new SimpleRegister(i * 100)); // 值为 0, 100, 200, ... + } + + // 2.4 初始化输入寄存器(Input Register,功能码 04)- 只读 + // 地址 0-19,共 20 个寄存器 + SimpleInputRegister[] inputRegisters = new SimpleInputRegister[20]; + for (int i = 0; i < 20; i++) { + inputRegisters[i] = new SimpleInputRegister(i * 10 + 1); // 值为 1, 11, 21, ... + spi.addInputRegister(inputRegisters[i]); + } + + // 3.1 创建 Modbus TCP 从站 + ModbusSlave slave = ModbusSlaveFactory.createTCPSlave(PORT, 5); + slave.addProcessImage(SLAVE_ID, spi); + // 3.2 启动从站 + slave.open(); + + System.out.println("==================================================="); + System.out.println("Modbus TCP 从站模拟器已启动"); + System.out.println("端口: " + PORT); + System.out.println("从站地址 (Slave ID): " + SLAVE_ID); + System.out.println("==================================================="); + System.out.println("可用寄存器:"); + System.out.println(" - 线圈 (Coil, 功能码 01/05): 地址 0-9"); + System.out.println(" - 离散输入 (Discrete Input, 功能码 02): 地址 0-9"); + System.out.println(" - 保持寄存器 (Holding Register, 功能码 03/06/16): 地址 0-19"); + System.out.println(" - 输入寄存器 (Input Register, 功能码 04): 地址 0-19"); + System.out.println("==================================================="); + System.out.println("按 Ctrl+C 停止模拟器"); + + // 4. 添加关闭钩子 + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("\n正在关闭模拟器..."); + slave.close(); + System.out.println("模拟器已关闭"); + })); + + // 5. 保持运行,定时更新输入寄存器模拟数据变化 + int counter = 0; + while (true) { + Thread.sleep(5000); // 每 5 秒更新一次 + counter++; + + // 更新输入寄存器的值,模拟传感器数据变化 + for (int i = 0; i < 20; i++) { + int newValue = (i * 10 + 1) + counter; + inputRegisters[i].setValue(newValue); + } + + // 更新保持寄存器的第一个值 + spi.getRegister(0).setValue(counter * 100); +// System.out.println("[" + java.time.LocalTime.now() + "] 数据已更新, counter=" + counter +// + ", 保持寄存器[0]=" + (counter * 100) +// + ", 输入寄存器[0]=" + (1 + counter)); + } + } + +} From 809cb452923c862e6223613fd3687360262e036c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 18 Jan 2026 00:08:58 +0800 Subject: [PATCH 06/53] =?UTF-8?q?feat=EF=BC=9A=E3=80=90iot=E3=80=91modbus-?= =?UTF-8?q?tcp=20=E5=8D=8F=E8=AE=AE=E6=8E=A5=E5=85=A5=20100%=EF=BC=9A?= =?UTF-8?q?=E5=92=8C=E7=AE=A1=E7=90=86=E5=90=8E=E5=8F=B0=E5=AF=B9=E6=8E=A5?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=B0=91=E9=87=8F=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/api/device/IoTDeviceApiImpl.java | 78 +------------------ .../IotDeviceModbusConfigController.java | 13 +--- .../IotDeviceModbusPointController.java | 10 +-- .../device/IotDeviceModbusConfigService.java | 7 -- .../IotDeviceModbusConfigServiceImpl.java | 17 ---- .../IotDeviceModbusPointServiceImpl.java | 3 +- .../tcp/IotTcpBinaryDeviceMessageCodec.java | 2 +- .../tcp/IotTcpJsonDeviceMessageCodec.java | 2 +- .../resources/tcp-binary-packet-examples.md | 2 +- .../resources/tcp-json-packet-examples.md | 2 +- 10 files changed, 15 insertions(+), 121 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java index a1aca2adc1..4a4d194db9 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.enums.RpcConstants; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.*; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; @@ -22,7 +23,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -74,12 +74,8 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { @Override @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/enabled-configs") @PermitAll + @TenantIgnore public CommonResult> getEnabledModbusDeviceConfigs() { - // TODO @芋艿:临时 mock 数据,用于测试 ModbusTcpSlaveSimulator - if (true) { - return success(buildMockModbusConfigs()); - } - // 1. 获取所有启用的 Modbus 连接配置 List configList = modbusConfigService.getEnabledDeviceModbusConfigList(); if (CollUtil.isEmpty(configList)) { @@ -111,74 +107,4 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { return success(result); } - /** - * 构建 Mock Modbus 配置,对接 ModbusTcpSlaveSimulator - * - * 设备:productKey=4aymZgOTOOCrDKRT, deviceName=small - * 物模型字段:width, height, oneThree - */ - private List buildMockModbusConfigs() { - List configs = new ArrayList<>(); - - // 设备配置 - IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO(); - config.setDeviceId(1L); - config.setProductKey("4aymZgOTOOCrDKRT"); - config.setDeviceName("small"); - config.setIp("127.0.0.1"); - config.setPort(5020); // 对应 ModbusTcpSlaveSimulator 的端口 - config.setSlaveId(1); - config.setTimeout(3000); - config.setRetryInterval(5000); - - // 点位配置(对应物模型字段:width, height, oneThree) - List points = new ArrayList<>(); - - // 点位 1:width - 读取保持寄存器地址 0(功能码 03) - IotModbusPointRespDTO point1 = new IotModbusPointRespDTO(); - point1.setId(1L); - point1.setIdentifier("width"); - point1.setName("宽度"); - point1.setFunctionCode(3); // 读保持寄存器 - point1.setRegisterAddress(0); - point1.setRegisterCount(1); - point1.setByteOrder("AB"); - point1.setRawDataType("INT16"); - point1.setScale(BigDecimal.ONE); - point1.setPollInterval(3000); // 3 秒轮询 - points.add(point1); - - // 点位 2:height - 读取保持寄存器地址 1(功能码 03) - IotModbusPointRespDTO point2 = new IotModbusPointRespDTO(); - point2.setId(2L); - point2.setIdentifier("height"); - point2.setName("高度"); - point2.setFunctionCode(3); // 读保持寄存器 - point2.setRegisterAddress(1); - point2.setRegisterCount(1); - point2.setByteOrder("AB"); - point2.setRawDataType("INT16"); - point2.setScale(BigDecimal.ONE); - point2.setPollInterval(3000); // 3 秒轮询 - points.add(point2); - - // 点位 3:oneThree - 读取保持寄存器地址 2(功能码 03) - IotModbusPointRespDTO point3 = new IotModbusPointRespDTO(); - point3.setId(3L); - point3.setIdentifier("oneThree"); - point3.setName("一三"); - point3.setFunctionCode(3); // 读保持寄存器 - point3.setRegisterAddress(2); - point3.setRegisterCount(1); - point3.setByteOrder("AB"); - point3.setRawDataType("INT16"); - point3.setScale(BigDecimal.ONE); - point3.setPollInterval(3000); // 3 秒轮询 - points.add(point3); - - config.setPoints(points); - configs.add(config); - return configs; - } - } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java index bd42a6feae..576d904aca 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java @@ -28,26 +28,17 @@ public class IotDeviceModbusConfigController { @PostMapping("/save") @Operation(summary = "保存设备 Modbus 连接配置") - @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:create')") + @PreAuthorize("@ss.hasPermission('iot:device:update')") public CommonResult saveDeviceModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO saveReqVO) { modbusConfigService.saveDeviceModbusConfig(saveReqVO); return success(true); } - @DeleteMapping("/delete") - @Operation(summary = "删除设备 Modbus 连接配置") - @Parameter(name = "id", description = "编号", required = true) - @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:delete')") - public CommonResult deleteDeviceModbusConfig(@RequestParam("id") Long id) { - modbusConfigService.deleteDeviceModbusConfig(id); - return success(true); - } - @GetMapping("/get") @Operation(summary = "获得设备 Modbus 连接配置") @Parameter(name = "id", description = "编号", example = "1024") @Parameter(name = "deviceId", description = "设备编号", example = "2048") - @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") + @PreAuthorize("@ss.hasPermission('iot:device:query')") public CommonResult getDeviceModbusConfig( @RequestParam(value = "id", required = false) Long id, @RequestParam(value = "deviceId", required = false) Long deviceId) { diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java index 22d174364e..4e813d8bfd 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java @@ -30,14 +30,14 @@ public class IotDeviceModbusPointController { @PostMapping("/create") @Operation(summary = "创建设备 Modbus 点位配置") - @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:create')") + @PreAuthorize("@ss.hasPermission('iot:device:update')") public CommonResult createDeviceModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO createReqVO) { return success(modbusPointService.createDeviceModbusPoint(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新设备 Modbus 点位配置") - @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:update')") + @PreAuthorize("@ss.hasPermission('iot:device:update')") public CommonResult updateDeviceModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO updateReqVO) { modbusPointService.updateDeviceModbusPoint(updateReqVO); return success(true); @@ -46,7 +46,7 @@ public class IotDeviceModbusPointController { @DeleteMapping("/delete") @Operation(summary = "删除设备 Modbus 点位配置") @Parameter(name = "id", description = "编号", required = true) - @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:delete')") + @PreAuthorize("@ss.hasPermission('iot:device:update')") public CommonResult deleteDeviceModbusPoint(@RequestParam("id") Long id) { modbusPointService.deleteDeviceModbusPoint(id); return success(true); @@ -55,7 +55,7 @@ public class IotDeviceModbusPointController { @GetMapping("/get") @Operation(summary = "获得设备 Modbus 点位配置") @Parameter(name = "id", description = "编号", required = true, example = "1024") - @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") + @PreAuthorize("@ss.hasPermission('iot:device:query')") public CommonResult getDeviceModbusPoint(@RequestParam("id") Long id) { IotDeviceModbusPointDO modbusPoint = modbusPointService.getDeviceModbusPoint(id); return success(BeanUtils.toBean(modbusPoint, IotDeviceModbusPointRespVO.class)); @@ -63,7 +63,7 @@ public class IotDeviceModbusPointController { @GetMapping("/page") @Operation(summary = "获得设备 Modbus 点位配置分页") - @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") + @PreAuthorize("@ss.hasPermission('iot:device:query')") public CommonResult> getDeviceModbusPointPage(@Valid IotDeviceModbusPointPageReqVO pageReqVO) { PageResult pageResult = modbusPointService.getDeviceModbusPointPage(pageReqVO); return success(BeanUtils.toBean(pageResult, IotDeviceModbusPointRespVO.class)); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java index 0c2c3e2982..1bd17e4183 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java @@ -20,13 +20,6 @@ public interface IotDeviceModbusConfigService { */ void saveDeviceModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO saveReqVO); - /** - * 删除设备 Modbus 连接配置 - * - * @param id 编号 - */ - void deleteDeviceModbusConfig(Long id); - /** * 获得设备 Modbus 连接配置 * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java index 2447e4680a..91388d2cf6 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java @@ -11,9 +11,6 @@ import org.springframework.validation.annotation.Validated; import java.util.List; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODBUS_CONFIG_NOT_EXISTS; - /** * IoT 设备 Modbus 连接配置 Service 实现类 * @@ -46,20 +43,6 @@ public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigSe } } - @Override - public void deleteDeviceModbusConfig(Long id) { - // 校验存在 - validateDeviceModbusConfigExists(id); - // 删除 - modbusConfigMapper.deleteById(id); - } - - private void validateDeviceModbusConfigExists(Long id) { - if (modbusConfigMapper.selectById(id) == null) { - throw exception(DEVICE_MODBUS_CONFIG_NOT_EXISTS); - } - } - @Override public IotDeviceModbusConfigDO getDeviceModbusConfig(Long id) { return modbusConfigMapper.selectById(id); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java index 242e1c9f27..906697b15b 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java @@ -70,7 +70,8 @@ public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointServ validateDeviceModbusPointUnique(updateReqVO.getDeviceId(), thingModel.getIdentifier(), updateReqVO.getId()); // 2. 更新 - IotDeviceModbusPointDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusPointDO.class); + IotDeviceModbusPointDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusPointDO.class, + o -> o.setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName())); modbusPointMapper.updateById(updateObj); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java index 4f42a8c2f6..c1b6cc3912 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java @@ -41,7 +41,7 @@ import java.nio.charset.StandardCharsets; @Component public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { - public static final String TYPE = "TCP_BINARY"; + public static final String TYPE = "TcpBinary"; /** * 协议魔术字,用于协议识别 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java index 10ffbdf5c6..99082b4325 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java @@ -29,7 +29,7 @@ import org.springframework.stereotype.Component; @Component public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { - public static final String TYPE = "TCP_JSON"; + public static final String TYPE = "TcpJson"; @Data @NoArgsConstructor diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md index d6b2b3fdb5..8e2037892f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md @@ -162,7 +162,7 @@ E8 AF 81 E6 88 90 E5 8A 9F 22 ## 4. 编解码器标识 ```java -public static final String TYPE = "TCP_BINARY"; +public static final String TYPE = "TcpBinary"; ``` ## 5. 协议优势 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md index 09ef50cfe5..5bfa42b4d0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md @@ -161,7 +161,7 @@ TCP JSON 格式协议采用纯 JSON 格式进行数据传输,具有以下特 ## 4. 编解码器标识 ```java -public static final String TYPE = "TCP_JSON"; +public static final String TYPE = "TcpJson"; ``` ## 5. 协议优势 From e89fc2bfbd1ac7d88ff59bb4a580cecc564a20fb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 31 Jan 2026 22:41:30 +0800 Subject: [PATCH 07/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91http=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-module-iot/yudao-module-iot-biz/pom.xml | 12 - .../iot/core/enums/IotProtocolTypeEnum.java | 46 ++++ .../iot/core/enums/IotSerializeTypeEnum.java | 40 +++ .../core/messagebus/core/IotMessageBus.java | 10 + .../messagebus/core/IotMessageSubscriber.java | 12 + .../iot/core/mq/message/IotDeviceMessage.java | 4 +- .../config/IotGatewayConfiguration.java | 74 ++--- .../gateway/config/IotGatewayProperties.java | 232 +++++++++------- .../iot/gateway/protocol/IotProtocol.java | 52 ++++ .../IotProtocolDownstreamSubscriber.java | 79 ++++++ .../gateway/protocol/IotProtocolManager.java | 126 +++++++++ .../coap/IotCoapDownstreamSubscriber.java | 32 +-- .../coap/IotCoapUpstreamProtocol.java | 27 +- .../coap/router/IotCoapAuthHandler.java | 2 - .../coap/router/IotCoapRegisterHandler.java | 1 - .../emqx/IotEmqxDownstreamSubscriber.java | 52 +--- .../emqx/IotEmqxUpstreamProtocol.java | 39 ++- .../gateway/protocol/http/IotHttpConfig.java | 28 ++ .../http/IotHttpDownstreamSubscriber.java | 45 ---- .../protocol/http/IotHttpProtocol.java | 185 +++++++++++++ .../http/IotHttpUpstreamProtocol.java | 91 ------- .../IotHttpDownstreamSubscriber.java | 27 ++ .../upstream}/IotHttpAuthHandler.java | 33 +-- .../upstream}/IotHttpRegisterHandler.java | 22 +- .../upstream}/IotHttpRegisterSubHandler.java | 37 +-- .../upstream}/IotHttpUpstreamHandler.java | 28 +- .../http/router/IotHttpAbstractHandler.java | 24 +- .../mqtt/IotMqttDownstreamSubscriber.java | 65 +---- .../mqtt/IotMqttUpstreamProtocol.java | 27 +- .../tcp/IotTcpDownstreamSubscriber.java | 55 +--- .../protocol/tcp/IotTcpUpstreamProtocol.java | 30 ++- .../udp/IotUdpDownstreamSubscriber.java | 53 +--- .../protocol/udp/IotUdpUpstreamProtocol.java | 27 +- .../IotWebSocketDownstreamSubscriber.java | 53 +--- .../IotWebSocketUpstreamProtocol.java | 27 +- .../serialize/IotMessageSerializer.java | 38 +++ .../IotMessageSerializerManager.java | 60 +++++ .../serialize/binary/IotBinarySerializer.java | 254 ++++++++++++++++++ .../serialize/json/IotJsonSerializer.java | 37 +++ .../src/main/resources/application.yaml | 14 +- 40 files changed, 1467 insertions(+), 633 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/{router => handler/upstream}/IotHttpAuthHandler.java (76%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/{router => handler/upstream}/IotHttpRegisterHandler.java (69%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/{router => handler/upstream}/IotHttpRegisterSubHandler.java (68%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/{router => handler/upstream}/IotHttpUpstreamHandler.java (64%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializer.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializerManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/binary/IotBinarySerializer.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/json/IotJsonSerializer.java diff --git a/yudao-module-iot/yudao-module-iot-biz/pom.xml b/yudao-module-iot/yudao-module-iot-biz/pom.xml index a0fe16de48..02644cb02f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/pom.xml +++ b/yudao-module-iot/yudao-module-iot-biz/pom.xml @@ -101,18 +101,6 @@ spring-boot-starter-amqp true - - - - - - - - - - - - diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java new file mode 100644 index 0000000000..5fbd713a8d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 协议类型枚举 + * + * 用于定义传输层协议类型 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotProtocolTypeEnum implements ArrayValuable { + + TCP("tcp"), + UDP("udp"), + WEBSOCKET("websocket"), + HTTP("http"), + MQTT("mqtt"), + EMQX("emqx"), + COAP("coap"), + MODBUS_TCP("modbus_tcp"); + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new); + + /** + * 类型 + */ + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + + public static IotProtocolTypeEnum of(String type) { + return ArrayUtil.firstMatch(e -> e.getType().equals(type), values()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java new file mode 100644 index 0000000000..0f9400f362 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 序列化类型枚举 + * + * 用于定义设备消息的序列化格式 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotSerializeTypeEnum implements ArrayValuable { + + JSON("json"), + BINARY("binary"); + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotSerializeTypeEnum::getType).toArray(String[]::new); + + /** + * 类型 + */ + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + + public static IotSerializeTypeEnum of(String type) { + return ArrayUtil.firstMatch(e -> e.getType().equals(type), values()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java index c621467610..646eb36bc7 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java @@ -24,4 +24,14 @@ public interface IotMessageBus { */ void register(IotMessageSubscriber subscriber); + /** + * 取消注册消息订阅者 + * + * @param subscriber 订阅者 + */ + default void unregister(IotMessageSubscriber subscriber) { + // TODO 芋艿:暂时不实现,需求量不大,但是 + // throw new UnsupportedOperationException("取消注册消息订阅者功能,尚未实现"); + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java index 23a055325c..fb5c712396 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java @@ -26,4 +26,16 @@ public interface IotMessageSubscriber { */ void onMessage(T message); + /** + * 启动订阅 + */ + default void start() { + } + + /** + * 停止订阅 + */ + default void stop() { + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java index feed3eb2a2..813b360433 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java @@ -60,7 +60,7 @@ public class IotDeviceMessage { */ private String serverId; - // ========== codec(编解码)字段 ========== + // ========== serialize(序列化)相关字段 ========== /** * 请求编号 @@ -94,7 +94,7 @@ public class IotDeviceMessage { */ private String msg; - // ========== 基础方法:只传递"codec(编解码)字段" ========== + // ========== 基础方法:只传递"serialize(序列化)相关字段" ========== public static IotDeviceMessage requestOf(String method) { return requestOf(null, method, null); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index a4e93a84fd..d300fd4a77 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -1,13 +1,13 @@ package cn.iocoder.yudao.module.iot.gateway.config; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; @@ -15,12 +15,15 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstrea import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; @@ -31,35 +34,22 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; + @Configuration @EnableConfigurationProperties(IotGatewayProperties.class) @Slf4j public class IotGatewayConfiguration { - /** - * IoT 网关 HTTP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.http", name = "enabled", havingValue = "true") - @Slf4j - public static class HttpProtocolConfiguration { + @Bean + public IotMessageSerializerManager iotMessageSerializerManager() { + return new IotMessageSerializerManager(); + } - @Bean(name = "httpVertx", destroyMethod = "close") - public Vertx httpVertx() { - return Vertx.vertx(); - } - - @Bean - public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties, - @Qualifier("httpVertx") Vertx httpVertx) { - return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), httpVertx); - } - - @Bean - public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol, - IotMessageBus messageBus) { - return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus); - } + @Bean + public IotProtocolManager iotProtocolManager(IotGatewayProperties gatewayProperties, + IotMessageSerializerManager serializerManager, + IotMessageBus messageBus) { + return new IotProtocolManager(gatewayProperties, serializerManager, messageBus); } /** @@ -117,12 +107,17 @@ public class IotGatewayConfiguration { deviceService, messageService, connectionManager, tcpVertx); } + @Bean + public IotTcpDownstreamHandler iotTcpDownstreamHandler(IotDeviceMessageService messageService, + IotTcpConnectionManager connectionManager) { + return new IotTcpDownstreamHandler(messageService, connectionManager); + } + @Bean public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, - IotDeviceMessageService messageService, - IotTcpConnectionManager connectionManager, + IotTcpDownstreamHandler downstreamHandler, IotMessageBus messageBus) { - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus); + return new IotTcpDownstreamSubscriber(protocolHandler, downstreamHandler, messageBus); } } @@ -187,12 +182,18 @@ public class IotGatewayConfiguration { deviceService, messageService, sessionManager, udpVertx); } + @Bean + public IotUdpDownstreamHandler iotUdpDownstreamHandler(IotDeviceMessageService messageService, + IotUdpSessionManager sessionManager, + IotUdpUpstreamProtocol protocol) { + return new IotUdpDownstreamHandler(messageService, sessionManager, protocol); + } + @Bean public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler, - IotDeviceMessageService messageService, - IotUdpSessionManager sessionManager, + IotUdpDownstreamHandler downstreamHandler, IotMessageBus messageBus) { - return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus); + return new IotUdpDownstreamSubscriber(protocolHandler, downstreamHandler, messageBus); } } @@ -241,12 +242,17 @@ public class IotGatewayConfiguration { deviceService, messageService, connectionManager, websocketVertx); } + @Bean + public IotWebSocketDownstreamHandler iotWebSocketDownstreamHandler(IotDeviceMessageService messageService, + IotWebSocketConnectionManager connectionManager) { + return new IotWebSocketDownstreamHandler(messageService, connectionManager); + } + @Bean public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler, - IotDeviceMessageService messageService, - IotWebSocketConnectionManager connectionManager, + IotWebSocketDownstreamHandler downstreamHandler, IotMessageBus messageBus) { - return new IotWebSocketDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus); + return new IotWebSocketDownstreamSubscriber(protocolHandler, downstreamHandler, messageBus); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 9a86ee600d..907a0ae8d9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -1,5 +1,8 @@ package cn.iocoder.yudao.module.iot.gateway.config; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -24,10 +27,15 @@ public class IotGatewayProperties { private TokenProperties token; /** - * 协议配置 + * 协议配置(旧版,保持兼容) */ private ProtocolProperties protocol; + /** + * 协议实例列表(新版) + */ + private List protocols; + @Data public static class RpcProperties { @@ -88,11 +96,6 @@ public class IotGatewayProperties { */ private MqttProperties mqtt; - /** - * MQTT WebSocket 组件配置 - */ - private MqttWsProperties mqttWs; - /** * UDP 组件配置 */ @@ -422,102 +425,6 @@ public class IotGatewayProperties { } - @Data - public static class MqttWsProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * WebSocket 服务器端口(默认:8083) - */ - private Integer port = 8083; - - /** - * WebSocket 路径(默认:/mqtt) - */ - @NotEmpty(message = "WebSocket 路径不能为空") - private String path = "/mqtt"; - - /** - * 最大消息大小(字节) - */ - private Integer maxMessageSize = 8192; - - /** - * 连接超时时间(秒) - */ - private Integer connectTimeoutSeconds = 60; - - /** - * 保持连接超时时间(秒) - */ - private Integer keepAliveTimeoutSeconds = 300; - - /** - * 是否启用 SSL(wss://) - */ - private Boolean sslEnabled = false; - - /** - * SSL 配置 - */ - private SslOptions sslOptions = new SslOptions(); - - /** - * WebSocket 子协议(通常为 "mqtt" 或 "mqttv3.1") - */ - @NotEmpty(message = "WebSocket 子协议不能为空") - private String subProtocol = "mqtt"; - - /** - * 最大帧大小(字节) - */ - private Integer maxFrameSize = 65536; - - /** - * SSL 配置选项 - */ - @Data - public static class SslOptions { - - /** - * 密钥证书选项 - */ - private io.vertx.core.net.KeyCertOptions keyCertOptions; - - /** - * 信任选项 - */ - private io.vertx.core.net.TrustOptions trustOptions; - - /** - * SSL 证书路径 - */ - private String certPath; - - /** - * SSL 私钥路径 - */ - private String keyPath; - - /** - * 信任存储路径 - */ - private String trustStorePath; - - /** - * 信任存储密码 - */ - private String trustStorePassword; - - } - - } - @Data public static class UdpProperties { @@ -643,4 +550,125 @@ public class IotGatewayProperties { } + // TODO @AI:【暂时忽略】改成 ProtocolProperties + /** + * 协议实例配置 + */ + @Data + public static class ProtocolInstanceProperties { + + /** + * 协议实例 ID,如 "http-alink"、"tcp-binary" + */ + @NotEmpty(message = "协议实例 ID 不能为空") + private String id; + /** + * 是否启用 + */ + @NotNull(message = "是否启用不能为空") + private Boolean enabled = true; + /** + * 协议类型 + * + * @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum + */ + @NotEmpty(message = "协议类型不能为空") + private String type; + /** + * 服务端口 + */ + @NotNull(message = "服务端口不能为空") + private Integer port; + /** + * 序列化类型(可选) + * + * @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum + * + * 为什么是可选的呢? + * 1. {@link IotProtocolTypeEnum#HTTP}、${@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式 + * 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析 + */ + private String serialize; + + /** + * HTTP 协议配置 + */ + @Valid + private IotHttpConfig http; + + // TODO @AI:后续改下; + /** + * TCP 协议配置(后续扩展) + */ + @Valid + private TcpInstanceConfig tcp; + + } + + /** + * TCP 协议实例配置(后续扩展) + */ + @Data + public static class TcpInstanceConfig { + + /** + * 最大连接数 + */ + private Integer maxConnections = 1000; + + /** + * 心跳超时时间(毫秒) + */ + private Long keepAliveTimeoutMs = 30000L; + + /** + * 是否启用 SSL + */ + private Boolean sslEnabled = false; + + /** + * SSL 证书路径 + */ + private String sslCertPath; + + /** + * SSL 私钥路径 + */ + private String sslKeyPath; + + /** + * 拆包配置 + */ + private CodecConfig codec; + + /** + * TCP 拆包配置 + */ + @Data + public static class CodecConfig { + + /** + * 拆包类型:LENGTH_FIELD / DELIMITER + */ + private String type; + + /** + * LENGTH_FIELD: 偏移量 + */ + private Integer lengthFieldOffset; + + /** + * LENGTH_FIELD: 长度字段长度 + */ + private Integer lengthFieldLength; + + /** + * DELIMITER: 分隔符 + */ + private String delimiter; + + } + + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocol.java new file mode 100644 index 0000000000..bdfc28bc91 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocol.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol; + +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; + +/** + * IoT 协议接口 + * + * 定义传输层协议的生命周期管理 + * + * @author 芋道源码 + */ +public interface IotProtocol { + + /** + * 获取协议实例 ID + * + * @return 协议实例 ID,如 "http-alink"、"tcp-binary" + */ + String getId(); + + /** + * 获取服务器 ID(用于消息追踪,全局唯一) + * + * @return 服务器 ID + */ + String getServerId(); + + /** + * 获取协议类型 + * + * @return 协议类型枚举 + */ + IotProtocolTypeEnum getType(); + + /** + * 启动协议服务 + */ + void start(); + + /** + * 停止协议服务 + */ + void stop(); + + /** + * 检查协议服务是否正在运行 + * + * @return 是否正在运行 + */ + boolean isRunning(); + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java new file mode 100644 index 0000000000..2e2150f6f7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 协议下行消息订阅者抽象类 + * + * 负责接收来自消息总线的下行消息,并委托给子类进行业务处理 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Slf4j +public abstract class IotProtocolDownstreamSubscriber implements IotMessageSubscriber { + + private final IotProtocol protocol; + + private final IotMessageBus messageBus; + + @Override + public String getTopic() { + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); + } + + /** + * 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group + */ + @Override + public String getGroup() { + return getTopic(); + } + + @Override + public void start() { + messageBus.register(this); + log.info("[start][{} 下行消息订阅成功,Topic:{}]", protocol.getType().name(), getTopic()); + } + + @Override + public void stop() { + messageBus.unregister(this); + log.info("[stop][{} 下行消息订阅已停止,Topic:{}]", protocol.getType().name(), getTopic()); + } + + @Override + public void onMessage(IotDeviceMessage message) { + log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId()); + try { + // 1. 校验 + String method = message.getMethod(); + if (StrUtil.isBlank(method)) { + log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", + message.getId(), message.getDeviceId()); + return; + } + + // 2. 处理下行消息 + handleMessage(message); + } catch (Exception e) { + log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId(), e); + } + } + + /** + * 处理下行消息 + * + * @param message 下行消息 + */ + protected abstract void handleMessage(IotDeviceMessage message); + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java new file mode 100644 index 0000000000..2e339e4a73 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.SmartLifecycle; + +import java.util.ArrayList; +import java.util.List; + +/** + * IoT 协议管理器 + * + * 负责根据配置创建和管理协议实例 + * + * @author 芋道源码 + */ +@Slf4j +public class IotProtocolManager implements SmartLifecycle { + + private final IotGatewayProperties gatewayProperties; + + private final IotMessageSerializerManager serializerManager; + + private final IotMessageBus messageBus; + + private final List protocols = new ArrayList<>(); + + private volatile boolean running = false; + + public IotProtocolManager(IotGatewayProperties gatewayProperties, + IotMessageSerializerManager serializerManager, + IotMessageBus messageBus) { + this.gatewayProperties = gatewayProperties; + this.serializerManager = serializerManager; + this.messageBus = messageBus; + } + + @Override + public void start() { + if (running) { + return; + } + List protocolConfigs = gatewayProperties.getProtocols(); + if (CollUtil.isEmpty(protocolConfigs)) { + log.info("[start][没有配置协议实例,跳过启动]"); + return; + } + + for (IotGatewayProperties.ProtocolInstanceProperties config : protocolConfigs) { + if (BooleanUtil.isFalse(config.getEnabled())) { + log.info("[start][协议实例 {} 未启用,跳过]", config.getId()); + continue; + } + IotProtocol protocol = createProtocol(config); + if (protocol == null) { + continue; + } + protocol.start(); + protocols.add(protocol); + } + running = true; + log.info("[start][协议管理器启动完成,共启动 {} 个协议实例]", protocols.size()); + } + + @Override + public void stop() { + if (!running) { + return; + } + for (IotProtocol protocol : protocols) { + try { + protocol.stop(); + } catch (Exception e) { + log.error("[stop][协议实例 {} 停止失败]", protocol.getId(), e); + } + } + protocols.clear(); + running = false; + log.info("[stop][协议管理器已停止]"); + } + + @Override + public boolean isRunning() { + return running; + } + + /** + * 创建协议实例 + * + * @param config 协议实例配置 + * @return 协议实例 + */ + @SuppressWarnings({"SwitchStatementWithTooFewBranches", "EnhancedSwitchMigration"}) + private IotProtocol createProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getType()); + if (protocolType == null) { + log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getType()); + return null; + } + switch (protocolType) { + case HTTP: + return createHttpProtocol(config); + // TODO 后续添加其他协议类型 + default: + throw new IllegalArgumentException(String.format( + "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); + } + } + + /** + * 创建 HTTP 协议实例 + * + * @param config 协议实例配置 + * @return HTTP 协议实例 + */ + private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + return new IotHttpProtocol(config, messageBus); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java index d01cdc416c..8003602d86 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java @@ -1,11 +1,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.coap; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; import lombok.extern.slf4j.Slf4j; /** @@ -13,34 +10,17 @@ import lombok.extern.slf4j.Slf4j; * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j -public class IotCoapDownstreamSubscriber implements IotMessageSubscriber { +public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber { - private final IotCoapUpstreamProtocol protocol; - - private final IotMessageBus messageBus; - - @PostConstruct - public void init() { - messageBus.register(this); + public IotCoapDownstreamSubscriber(IotCoapUpstreamProtocol protocol, IotMessageBus messageBus) { + super(protocol, messageBus); } @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { + protected void handleMessage(IotDeviceMessage message) { // 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更) - log.warn("[onMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message); + log.warn("[handleMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java index e259aa69c7..771a33a955 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.coap; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthResource; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterHandler; @@ -30,7 +32,9 @@ import java.util.concurrent.TimeUnit; * @author 芋道源码 */ @Slf4j -public class IotCoapUpstreamProtocol { +public class IotCoapUpstreamProtocol implements IotProtocol { + + private static final String ID = "coap"; private final IotGatewayProperties.CoapProperties coapProperties; @@ -39,11 +43,24 @@ public class IotCoapUpstreamProtocol { @Getter private final String serverId; + private volatile boolean running = false; + public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) { this.coapProperties = coapProperties; this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort()); } + @Override + public String getId() { + return ID; + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.COAP; + } + + @Override @PostConstruct public void start() { try { @@ -73,6 +90,7 @@ public class IotCoapUpstreamProtocol { // 3. 启动服务器 coapServer.start(); + running = true; log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort()); } catch (Exception e) { log.error("[start][IoT 网关 CoAP 协议启动失败]", e); @@ -80,11 +98,13 @@ public class IotCoapUpstreamProtocol { } } + @Override @PreDestroy public void stop() { if (coapServer != null) { try { coapServer.stop(); + running = false; log.info("[stop][IoT 网关 CoAP 协议已停止]"); } catch (Exception e) { log.error("[stop][IoT 网关 CoAP 协议停止失败]", e); @@ -92,4 +112,9 @@ public class IotCoapUpstreamProtocol { } } + @Override + public boolean isRunning() { + return running; + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java index 43fb77608a..fa2cc04fdd 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java @@ -24,8 +24,6 @@ import java.util.Map; /** * IoT 网关 CoAP 协议的【认证】处理器 * - * 参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler} - * * @author 芋道源码 */ @Slf4j diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java index 8ffbe4f677..08f6cca7a1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java @@ -22,7 +22,6 @@ import java.util.Map; * * @author 芋道源码 * @see 阿里云 - 一型一密 - * @see cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler */ @Slf4j public class IotCoapRegisterHandler { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java index 61bf12376b..4b5bad2d59 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java @@ -1,11 +1,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler; -import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; /** @@ -14,55 +13,18 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber { +public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber { private final IotEmqxDownstreamHandler downstreamHandler; - private final IotMessageBus messageBus; - - private final IotEmqxUpstreamProtocol protocol; - public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) { - this.protocol = protocol; - this.messageBus = messageBus; + super(protocol, messageBus); this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); } - @PostConstruct - public void init() { - messageBus.register(this); - } - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); } - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - try { - // 1. 校验 - String method = message.getMethod(); - if (method == null) { - log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", - message.getId(), message.getDeviceId()); - return; - } - - // 2. 处理下行消息 - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId(), e); - } - } - -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java index a888158746..47b2f1646e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java @@ -2,8 +2,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; @@ -28,11 +30,13 @@ import java.util.concurrent.atomic.AtomicBoolean; * @author 芋道源码 */ @Slf4j -public class IotEmqxUpstreamProtocol { +public class IotEmqxUpstreamProtocol implements IotProtocol { + + private static final String ID = "emqx"; private final IotGatewayProperties.EmqxProperties emqxProperties; - private volatile boolean isRunning = false; + private volatile boolean running = false; private final Vertx vertx; @@ -50,9 +54,20 @@ public class IotEmqxUpstreamProtocol { this.vertx = vertx; } + @Override + public String getId() { + return ID; + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.EMQX; + } + + @Override @PostConstruct public void start() { - if (isRunning) { + if (running) { return; } @@ -61,7 +76,7 @@ public class IotEmqxUpstreamProtocol { startMqttClient(); // 2. 标记服务为运行状态 - isRunning = true; + running = true; log.info("[start][IoT 网关 EMQX 协议启动成功]"); } catch (Exception e) { log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e); @@ -88,9 +103,10 @@ public class IotEmqxUpstreamProtocol { } } + @Override @PreDestroy public void stop() { - if (!isRunning) { + if (!running) { return; } @@ -98,10 +114,15 @@ public class IotEmqxUpstreamProtocol { stopMqttClient(); // 2. 标记服务为停止状态 - isRunning = false; + running = false; log.info("[stop][IoT 网关 MQTT 协议服务已停止]"); } + @Override + public boolean isRunning() { + return running; + } + /** * 启动 MQTT 客户端 */ @@ -185,7 +206,7 @@ public class IotEmqxUpstreamProtocol { * 延迟重连 */ private void reconnectWithDelay() { - if (!isRunning) { + if (!running) { return; } if (mqttClient != null && mqttClient.isConnected()) { @@ -195,7 +216,7 @@ public class IotEmqxUpstreamProtocol { long delay = emqxProperties.getReconnectDelayMs(); log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay); vertx.setTimer(delay, timerId -> { - if (!isRunning) { + if (!running) { return; } if (mqttClient != null && mqttClient.isConnected()) { @@ -305,7 +326,7 @@ public class IotEmqxUpstreamProtocol { private void setupMqttHandlers() { // 1. 设置断开重连监听器 mqttClient.closeHandler(closeEvent -> { - if (!isRunning) { + if (!running) { return; } log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java new file mode 100644 index 0000000000..968a9ae625 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http; + +import lombok.Data; + +/** + * IoT HTTP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotHttpConfig { + + /** + * 是否启用 SSL + */ + private Boolean sslEnabled = false; + + /** + * SSL 证书路径 + */ + private String sslCertPath; + + /** + * SSL 私钥路径 + */ + private String sslKeyPath; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java deleted file mode 100644 index 585bbdd30b..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 HTTP 订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class IotHttpDownstreamSubscriber implements IotMessageSubscriber { - - private final IotHttpUpstreamProtocol protocol; - - private final IotMessageBus messageBus; - - @PostConstruct - public void init() { - messageBus.register(this); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - log.info("[onMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java new file mode 100644 index 0000000000..c65e8d87ab --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java @@ -0,0 +1,185 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http; + +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterSubHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpUpstreamHandler; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT HTTP 协议实现 + *

+ * 基于 Vert.x 实现 HTTP 服务器,接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotHttpProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolInstanceProperties properties; + /** + * 消息总线 + */ + private final IotMessageBus messageBus; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * Vert.x 实例(每个 Protocol 自己管理) + */ + private Vertx vertx; + /** + * HTTP 服务器 + */ + private HttpServer httpServer; + /** + * 下行消息订阅者 + */ + private IotHttpDownstreamSubscriber downstreamSubscriber; + + /** + * 运行状态 + */ + private volatile boolean running = false; + + public IotHttpProtocol(ProtocolInstanceProperties properties, IotMessageBus messageBus) { + this.properties = properties; + this.messageBus = messageBus; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.HTTP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT HTTP 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例(每个 Protocol 独立管理) + this.vertx = Vertx.vertx(); + + // 1.2 创建路由 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); + + // 1.3 创建处理器,添加路由处理器 + IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); + router.post(IotHttpAuthHandler.PATH).handler(authHandler); + IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler(); + router.post(IotHttpRegisterHandler.PATH).handler(registerHandler); + IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler(); + router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler); + IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); + router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); + + // 1.4 启动 HTTP 服务器 + IotHttpConfig httpConfig = properties.getHttp(); + HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); + if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) { + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(httpConfig.getSslKeyPath()) + .setCertPath(httpConfig.getSslCertPath()); + options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + try { + httpServer = vertx.createHttpServer(options) + .requestHandler(router) + .listen() + .result(); + running = true; + log.info("[start][IoT HTTP 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); + // 启动失败时关闭 Vertx + if (vertx != null) { + vertx.close(); + vertx = null; + } + throw e; + } + + // 2. 启动下行消息订阅者 + this.downstreamSubscriber.start(); + } + + @Override + public void stop() { + if (!running) { + return; + } + // 1. 停止下行消息订阅者 + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT HTTP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT HTTP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; + } + + // 2.1 关闭 HTTP 服务器 + if (httpServer != null) { + try { + httpServer.close().result(); + log.info("[stop][IoT HTTP 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT HTTP 协议 {} 服务器停止失败]", getId(), e); + } + httpServer = null; + } + // 2.2 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT HTTP 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT HTTP 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT HTTP 协议 {} 已停止]", getId()); + } + + @Override + public boolean isRunning() { + return running; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java deleted file mode 100644 index 54cb2da1f4..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java +++ /dev/null @@ -1,91 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http; - -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.net.PemKeyCertOptions; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 HTTP 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotHttpUpstreamProtocol { - - private final IotGatewayProperties.HttpProperties httpProperties; - - private final Vertx vertx; - - private HttpServer httpServer; - - @Getter - private final String serverId; - - public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties, Vertx vertx) { - this.httpProperties = httpProperties; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort()); - } - - @PostConstruct - public void start() { - // 创建路由 - Router router = Router.router(vertx); - router.route().handler(BodyHandler.create()); - - // 创建处理器,添加路由处理器 - IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); - router.post(IotHttpAuthHandler.PATH).handler(authHandler); - IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler(); - router.post(IotHttpRegisterHandler.PATH).handler(registerHandler); - IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler(); - router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler); - IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); - router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); - - // 启动 HTTP 服务器 - HttpServerOptions options = new HttpServerOptions() - .setPort(httpProperties.getServerPort()); - if (Boolean.TRUE.equals(httpProperties.getSslEnabled())) { - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions().setKeyPath(httpProperties.getSslKeyPath()) - .setCertPath(httpProperties.getSslCertPath()); - options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); - } - try { - httpServer = vertx.createHttpServer(options) - .requestHandler(router) - .listen() - .result(); - log.info("[start][IoT 网关 HTTP 协议启动成功,端口:{}]", httpProperties.getServerPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 HTTP 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (httpServer != null) { - try { - httpServer.close().result(); - log.info("[stop][IoT 网关 HTTP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 HTTP 协议停止失败]", e); - } - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java new file mode 100644 index 0000000000..bfac16ca5e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 HTTP 订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ + +@Slf4j +public class IotHttpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { + + public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) { + super(protocol, messageBus); + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + log.info("[handleMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java similarity index 76% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java index 148756ca8b..0cc8e35554 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; @@ -10,10 +10,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler; import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -32,7 +32,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { public static final String PATH = "/auth"; - private final IotHttpUpstreamProtocol protocol; + private final String serverId; private final IotDeviceTokenService deviceTokenService; @@ -40,8 +40,8 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { private final IotDeviceMessageService deviceMessageService; - public IotHttpAuthHandler(IotHttpUpstreamProtocol protocol) { - this.protocol = protocol; + public IotHttpAuthHandler(IotHttpProtocol protocol) { + this.serverId = protocol.getServerId(); this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); @@ -50,32 +50,25 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { @Override public CommonResult handle0(RoutingContext context) { // 1. 解析参数 - JsonObject body = context.body().asJsonObject(); - if (body == null) { - throw invalidParamException("请求体不能为空"); - } - String clientId = body.getString("clientId"); - if (StrUtil.isEmpty(clientId)) { + IotDeviceAuthReqDTO request = deserializeRequest(context, IotDeviceAuthReqDTO.class); + if (StrUtil.isEmpty(request.getClientId())) { throw invalidParamException("clientId 不能为空"); } - String username = body.getString("username"); - if (StrUtil.isEmpty(username)) { + if (StrUtil.isEmpty(request.getUsername())) { throw invalidParamException("username 不能为空"); } - String password = body.getString("password"); - if (StrUtil.isEmpty(password)) { + if (StrUtil.isEmpty(request.getPassword())) { throw invalidParamException("password 不能为空"); } // 2.1 执行认证 - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(clientId).setUsername(username).setPassword(password)); + CommonResult result = deviceApi.authDevice(request); result.checkError(); if (!BooleanUtil.isTrue(result.getData())) { throw exception(DEVICE_AUTH_FAIL); } // 2.2 生成 Token - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username); + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername()); Assert.notNull(deviceInfo, "设备信息不能为空"); String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); Assert.notBlank(token, "生成 token 不能为空位"); @@ -83,7 +76,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { // 3. 执行上线 IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); deviceMessageService.sendDeviceMessage(message, - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId()); + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); // 构建响应数据 return success(MapUtil.of("token", token)); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java similarity index 69% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java index 51459dfa26..ec3bd54b4e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; @@ -6,7 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import io.vertx.core.json.JsonObject; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler; import io.vertx.ext.web.RoutingContext; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; @@ -33,27 +33,19 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler { @Override public CommonResult handle0(RoutingContext context) { // 1. 解析参数 - JsonObject body = context.body().asJsonObject(); - if (body == null) { - throw invalidParamException("请求体不能为空"); - } - String productKey = body.getString("productKey"); - if (StrUtil.isEmpty(productKey)) { + IotDeviceRegisterReqDTO request = deserializeRequest(context, IotDeviceRegisterReqDTO.class); + if (StrUtil.isEmpty(request.getProductKey())) { throw invalidParamException("productKey 不能为空"); } - String deviceName = body.getString("deviceName"); - if (StrUtil.isEmpty(deviceName)) { + if (StrUtil.isEmpty(request.getDeviceName())) { throw invalidParamException("deviceName 不能为空"); } - String productSecret = body.getString("productSecret"); - if (StrUtil.isEmpty(productSecret)) { + if (StrUtil.isEmpty(request.getProductSecret())) { throw invalidParamException("productSecret 不能为空"); } // 2. 调用动态注册 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() - .setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret); - CommonResult result = deviceApi.registerDevice(reqDTO); + CommonResult result = deviceApi.registerDevice(request); result.checkError(); // 3. 返回结果 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java similarity index 68% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java index 32a6144b7a..914de1b795 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java @@ -1,13 +1,15 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; +import cn.hutool.core.collection.CollUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; -import io.vertx.core.json.JsonObject; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler; import io.vertx.ext.web.RoutingContext; +import lombok.Data; import java.util.List; @@ -39,29 +41,32 @@ public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler { @Override public CommonResult handle0(RoutingContext context) { - // 1. 解析通用参数 + // 1.1 解析通用参数 String productKey = context.pathParam("productKey"); String deviceName = context.pathParam("deviceName"); - - // 2. 解析子设备列表 - JsonObject body = context.body().asJsonObject(); - if (body == null) { - throw invalidParamException("请求体不能为空"); - } - if (body.getJsonArray("params") == null) { + // 1.2 解析子设备列表 + SubDeviceRegisterRequest request = deserializeRequest(context, SubDeviceRegisterRequest.class); + if (CollUtil.isEmpty(request.getParams())) { throw invalidParamException("params 不能为空"); } - List subDevices = JsonUtils.parseArray( - body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class); - // 3. 调用子设备动态注册 + // 2. 调用子设备动态注册 IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO() - .setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices); + .setGatewayProductKey(productKey) + .setGatewayDeviceName(deviceName) + .setSubDevices(request.getParams()); CommonResult> result = deviceApi.registerSubDevices(reqDTO); result.checkError(); - // 4. 返回结果 + // 3. 返回结果 return success(result.getData()); } + @Data + public static class SubDeviceRegisterRequest { + + private List params; + + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java similarity index 64% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java index 5289e03a1f..d7f307bb8d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; @@ -6,31 +6,28 @@ import cn.hutool.core.text.StrPool; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; - /** * IoT 网关 HTTP 协议的【上行】处理器 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public class IotHttpUpstreamHandler extends IotHttpAbstractHandler { public static final String PATH = "/topic/sys/:productKey/:deviceName/*"; - private final IotHttpUpstreamProtocol protocol; + private final String serverId; private final IotDeviceMessageService deviceMessageService; - public IotHttpUpstreamHandler(IotHttpUpstreamProtocol protocol) { - this.protocol = protocol; + public IotHttpUpstreamHandler(IotHttpProtocol protocol) { + this.serverId = protocol.getServerId(); this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); } @@ -41,20 +38,15 @@ public class IotHttpUpstreamHandler extends IotHttpAbstractHandler { String deviceName = context.pathParam("deviceName"); String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT); - // 2.1 解析消息 - if (context.body().buffer() == null) { - throw invalidParamException("请求体不能为空"); - } - byte[] bytes = context.body().buffer().getBytes(); - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes, - productKey, deviceName); + // 2.1 根据 Content-Type 反序列化消息 + IotDeviceMessage message = deserializeRequest(context, IotDeviceMessage.class); Assert.equals(method, message.getMethod(), "method 不匹配"); // 2.2 发送消息 deviceMessageService.sendDeviceMessage(message, - productKey, deviceName, protocol.getServerId()); + productKey, deviceName, serverId); // 3. 返回结果 return CommonResult.success(MapUtil.of("messageId", message.getId())); } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java index 850fde1878..dbc93a927e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; @@ -9,11 +10,13 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterHandler; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; import io.vertx.core.Handler; import io.vertx.core.http.HttpHeaders; import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; @@ -27,12 +30,13 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public abstract class IotHttpAbstractHandler implements Handler { private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + private final IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); + @Override public final void handle(RoutingContext context) { try { @@ -83,12 +87,26 @@ public abstract class IotHttpAbstractHandler implements Handler } } + // ========== 序列化相关方法 ========== + + protected static T deserializeRequest(RoutingContext context, Class clazz) { + byte[] body = context.body().buffer() != null ? context.body().buffer().getBytes() : null; + if (ArrayUtil.isEmpty(body)) { + throw invalidParamException("请求体不能为空"); + } + return JsonUtils.parseObject(body, clazz); + } + + private static String serializeResponse(Object data) { + return JsonUtils.toJsonString(data); + } + @SuppressWarnings("deprecation") public static void writeResponse(RoutingContext context, Object data) { context.response() .setStatusCode(200) .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) - .end(JsonUtils.toJsonString(data)); + .end(serializeResponse(data)); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java index 3b62368fd9..fe9b600b99 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java @@ -1,11 +1,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; -import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; /** @@ -16,64 +14,27 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotMqttDownstreamSubscriber implements IotMessageSubscriber { - - private final IotMqttUpstreamProtocol upstreamProtocol; +public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber { private final IotMqttDownstreamHandler downstreamHandler; - private final IotMessageBus messageBus; - - public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol upstreamProtocol, + public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol protocol, IotMqttDownstreamHandler downstreamHandler, IotMessageBus messageBus) { - this.upstreamProtocol = upstreamProtocol; + super(protocol, messageBus); this.downstreamHandler = downstreamHandler; - this.messageBus = messageBus; - } - - @PostConstruct - public void subscribe() { - messageBus.register(this); - log.info("[subscribe][MQTT 协议下行消息订阅成功,主题:{}]", getTopic()); } @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - try { - // 1. 校验 - String method = message.getMethod(); - if (method == null) { - log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", - message.getId(), message.getDeviceId()); - return; - } - - // 2. 委托给下行处理器处理业务逻辑 - boolean success = downstreamHandler.handleDownstreamMessage(message); - if (success) { - log.debug("[onMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - } else { - log.warn("[onMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - } - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId(), e); + protected void handleMessage(IotDeviceMessage message) { + boolean success = downstreamHandler.handleDownstreamMessage(message); + if (success) { + log.debug("[handleMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId()); + } else { + log.warn("[handleMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId()); } } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java index fc0b6672c1..46fbc7c3fa 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; @@ -19,7 +21,11 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotMqttUpstreamProtocol { +public class IotMqttUpstreamProtocol implements IotProtocol { + + private static final String ID = "mqtt"; + + private volatile boolean running = false; private final IotGatewayProperties.MqttProperties mqttProperties; @@ -45,7 +51,23 @@ public class IotMqttUpstreamProtocol { this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort()); } + @Override + public String getId() { + return ID; + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.MQTT; + } + + @Override + public boolean isRunning() { + return running; + } + // TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈; + @Override @PostConstruct public void start() { // 创建服务器选项 @@ -71,6 +93,7 @@ public class IotMqttUpstreamProtocol { // 启动服务器 try { mqttServer.listen().result(); + running = true; log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort()); } catch (Exception e) { log.error("[start][IoT 网关 MQTT 协议启动失败]", e); @@ -78,11 +101,13 @@ public class IotMqttUpstreamProtocol { } } + @Override @PreDestroy public void stop() { if (mqttServer != null) { try { mqttServer.close().result(); + running = false; log.info("[stop][IoT 网关 MQTT 协议已停止]"); } catch (Exception e) { log.error("[stop][IoT 网关 MQTT 协议停止失败]", e); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java index 3f0cc02bcf..f04d2b2381 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java @@ -1,14 +1,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** @@ -17,48 +12,20 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -@RequiredArgsConstructor -public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { +public class IotTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { - private final IotTcpUpstreamProtocol protocol; + private final IotTcpDownstreamHandler downstreamHandler; - private final IotDeviceMessageService messageService; - - private final IotTcpConnectionManager connectionManager; - - private final IotMessageBus messageBus; - - private IotTcpDownstreamHandler downstreamHandler; - - @PostConstruct - public void init() { - // 初始化下游处理器 - this.downstreamHandler = new IotTcpDownstreamHandler(messageService, connectionManager); - // 注册下游订阅者 - messageBus.register(this); - log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", - protocol.getServerId(), getTopic()); + public IotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocol, + IotTcpDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; } @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); } - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId(), e); - } - } - -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java index 791c6cbfc2..e5aeb78c08 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; @@ -21,7 +23,9 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotTcpUpstreamProtocol { +public class IotTcpUpstreamProtocol implements IotProtocol { + + private static final String ID = "tcp"; private final IotGatewayProperties.TcpProperties tcpProperties; @@ -38,6 +42,8 @@ public class IotTcpUpstreamProtocol { private NetServer tcpServer; + private volatile boolean running = false; + public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, IotDeviceService deviceService, IotDeviceMessageService messageService, @@ -51,6 +57,17 @@ public class IotTcpUpstreamProtocol { this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); } + @Override + public String getId() { + return ID; + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.TCP; + } + + @Override @PostConstruct public void start() { // 创建服务器选项 @@ -78,6 +95,7 @@ public class IotTcpUpstreamProtocol { // 启动服务器 try { tcpServer.listen().result(); + running = true; log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); } catch (Exception e) { log.error("[start][IoT 网关 TCP 协议启动失败]", e); @@ -85,15 +103,23 @@ public class IotTcpUpstreamProtocol { } } + @Override @PreDestroy public void stop() { if (tcpServer != null) { try { tcpServer.close().result(); + running = false; log.info("[stop][IoT 网关 TCP 协议已停止]"); } catch (Exception e) { log.error("[stop][IoT 网关 TCP 协议停止失败]", e); } } } -} \ No newline at end of file + + @Override + public boolean isRunning() { + return running; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java index 87f8785515..e11380c73b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java @@ -1,14 +1,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** @@ -17,48 +12,20 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -@RequiredArgsConstructor -public class IotUdpDownstreamSubscriber implements IotMessageSubscriber { +public class IotUdpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { - private final IotUdpUpstreamProtocol protocol; + private final IotUdpDownstreamHandler downstreamHandler; - private final IotDeviceMessageService messageService; - - private final IotUdpSessionManager sessionManager; - - private final IotMessageBus messageBus; - - private IotUdpDownstreamHandler downstreamHandler; - - @PostConstruct - public void init() { - // 初始化下游处理器 - this.downstreamHandler = new IotUdpDownstreamHandler(messageService, sessionManager, protocol); - // 注册下游订阅者 - messageBus.register(this); - log.info("[init][UDP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", - protocol.getServerId(), getTopic()); + public IotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocol, + IotUdpDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; } @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId(), e); - } + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java index 7448683890..294368f291 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java @@ -2,9 +2,11 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; @@ -30,7 +32,9 @@ import java.util.List; * @author 芋道源码 */ @Slf4j -public class IotUdpUpstreamProtocol { +public class IotUdpUpstreamProtocol implements IotProtocol { + + private static final String ID = "udp"; private final IotGatewayProperties.UdpProperties udpProperties; @@ -55,6 +59,8 @@ public class IotUdpUpstreamProtocol { private IotUdpUpstreamHandler upstreamHandler; + private volatile boolean running = false; + public IotUdpUpstreamProtocol(IotGatewayProperties.UdpProperties udpProperties, IotDeviceService deviceService, IotDeviceMessageService messageService, @@ -68,6 +74,17 @@ public class IotUdpUpstreamProtocol { this.serverId = IotDeviceMessageUtils.generateServerId(udpProperties.getPort()); } + @Override + public String getId() { + return ID; + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.UDP; + } + + @Override @PostConstruct public void start() { // 1. 初始化上行消息处理器 @@ -90,6 +107,7 @@ public class IotUdpUpstreamProtocol { } // 设置数据包处理器 udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); + running = true; log.info("[start][IoT 网关 UDP 协议启动成功,端口:{},接收缓冲区:{} 字节,发送缓冲区:{} 字节]", udpProperties.getPort(), udpProperties.getReceiveBufferSize(), udpProperties.getSendBufferSize()); @@ -99,6 +117,7 @@ public class IotUdpUpstreamProtocol { }); } + @Override @PreDestroy public void stop() { // 1. 取消会话清理定时器 @@ -112,6 +131,7 @@ public class IotUdpUpstreamProtocol { if (udpSocket != null) { try { udpSocket.close().result(); + running = false; log.info("[stop][IoT 网关 UDP 协议已停止]"); } catch (Exception e) { log.error("[stop][IoT 网关 UDP 协议停止失败]", e); @@ -119,6 +139,11 @@ public class IotUdpUpstreamProtocol { } } + @Override + public boolean isRunning() { + return running; + } + /** * 启动会话清理定时器 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java index 47abb331ad..4b11bb02be 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java @@ -1,14 +1,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** @@ -17,48 +12,20 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -@RequiredArgsConstructor -public class IotWebSocketDownstreamSubscriber implements IotMessageSubscriber { +public class IotWebSocketDownstreamSubscriber extends IotProtocolDownstreamSubscriber { - private final IotWebSocketUpstreamProtocol protocol; + private final IotWebSocketDownstreamHandler downstreamHandler; - private final IotDeviceMessageService messageService; - - private final IotWebSocketConnectionManager connectionManager; - - private final IotMessageBus messageBus; - - private IotWebSocketDownstreamHandler downstreamHandler; - - @PostConstruct - public void init() { - // 初始化下游处理器 - this.downstreamHandler = new IotWebSocketDownstreamHandler(messageService, connectionManager); - // 注册下游订阅者 - messageBus.register(this); - log.info("[init][WebSocket 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", - protocol.getServerId(), getTopic()); + public IotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocol, + IotWebSocketDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; } @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId(), e); - } + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java index 9c612acec5..75465954da 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java @@ -1,8 +1,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; @@ -22,7 +24,9 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotWebSocketUpstreamProtocol { +public class IotWebSocketUpstreamProtocol implements IotProtocol { + + private static final String ID = "websocket"; private final IotGatewayProperties.WebSocketProperties wsProperties; @@ -39,6 +43,8 @@ public class IotWebSocketUpstreamProtocol { private HttpServer httpServer; + private volatile boolean running = false; + public IotWebSocketUpstreamProtocol(IotGatewayProperties.WebSocketProperties wsProperties, IotDeviceService deviceService, IotDeviceMessageService messageService, @@ -52,6 +58,17 @@ public class IotWebSocketUpstreamProtocol { this.serverId = IotDeviceMessageUtils.generateServerId(wsProperties.getPort()); } + @Override + public String getId() { + return ID; + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.WEBSOCKET; + } + + @Override @PostConstruct @SuppressWarnings("deprecation") public void start() { @@ -88,6 +105,7 @@ public class IotWebSocketUpstreamProtocol { // 3. 启动服务器 try { httpServer.listen().result(); + running = true; log.info("[start][IoT 网关 WebSocket 协议启动成功,端口:{},路径:{}]", wsProperties.getPort(), wsProperties.getPath()); } catch (Exception e) { log.error("[start][IoT 网关 WebSocket 协议启动失败]", e); @@ -95,11 +113,13 @@ public class IotWebSocketUpstreamProtocol { } } + @Override @PreDestroy public void stop() { if (httpServer != null) { try { httpServer.close().result(); + running = false; log.info("[stop][IoT 网关 WebSocket 协议已停止]"); } catch (Exception e) { log.error("[stop][IoT 网关 WebSocket 协议停止失败]", e); @@ -107,4 +127,9 @@ public class IotWebSocketUpstreamProtocol { } } + @Override + public boolean isRunning() { + return running; + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializer.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializer.java new file mode 100644 index 0000000000..095dae0b6d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializer.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.gateway.serialize; + +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; + +/** + * IoT 设备消息序列化器接口 + * + * 用于序列化和反序列化设备消息 + * + * @author 芋道源码 + */ +public interface IotMessageSerializer { + + /** + * 序列化消息 + * + * @param message 消息 + * @return 编码后的消息内容 + */ + byte[] serialize(IotDeviceMessage message); + + /** + * 反序列化消息 + * + * @param bytes 消息内容 + * @return 解码后的消息内容 + */ + IotDeviceMessage deserialize(byte[] bytes); + + /** + * 获取序列化类型 + * + * @return 序列化类型枚举 + */ + IotSerializeTypeEnum getType(); + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializerManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializerManager.java new file mode 100644 index 0000000000..0c072a5a1f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializerManager.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.gateway.serialize; + +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.serialize.binary.IotBinarySerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; +import lombok.extern.slf4j.Slf4j; + +import java.util.EnumMap; +import java.util.Map; + +/** + * IoT 序列化器管理器 + * + * 负责根据枚举创建和管理序列化器实例 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMessageSerializerManager { + + private final Map serializerMap = new EnumMap<>(IotSerializeTypeEnum.class); + + public IotMessageSerializerManager() { + // 遍历枚举,创建对应的序列化器 + for (IotSerializeTypeEnum type : IotSerializeTypeEnum.values()) { + IotMessageSerializer serializer = createSerializer(type); + serializerMap.put(type, serializer); + log.info("[IotSerializerManager][序列化器 {} 创建成功]", type); + } + } + + /** + * 根据类型创建序列化器 + * + * @param type 序列化类型 + * @return 序列化器实例 + */ + @SuppressWarnings("EnhancedSwitchMigration") + private IotMessageSerializer createSerializer(IotSerializeTypeEnum type) { + switch (type) { + case JSON: + return new IotJsonSerializer(); + case BINARY: + return new IotBinarySerializer(); + default: + throw new IllegalArgumentException("未知的序列化类型:" + type); + } + } + + /** + * 获取序列化器 + * + * @param type 序列化类型 + * @return 序列化器实例 + */ + public IotMessageSerializer get(IotSerializeTypeEnum type) { + return serializerMap.get(type); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/binary/IotBinarySerializer.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/binary/IotBinarySerializer.java new file mode 100644 index 0000000000..7227c4d7f9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/binary/IotBinarySerializer.java @@ -0,0 +1,254 @@ +package cn.iocoder.yudao.module.iot.gateway.serialize.binary; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import io.vertx.core.buffer.Buffer; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; + +/** + * 二进制格式的消息序列化器 + * + * 二进制协议格式(所有数值使用大端序): + * + *
+ * +--------+--------+--------+---------------------------+--------+--------+
+ * | 魔术字 | 版本号 | 消息类型|         消息长度(4 字节)          |
+ * +--------+--------+--------+---------------------------+--------+--------+
+ * |           消息 ID 长度(2 字节)        |      消息 ID (变长字符串)         |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * |           方法名长度(2 字节)        |      方法名(变长字符串)         |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * |                        消息体数据(变长)                              |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * 
+ * + * 消息体格式: + * - 请求消息:params 数据(JSON) + * - 响应消息:code (4字节) + msg 长度(2字节) + msg 字符串 + data 数据(JSON) + * + * @author 芋道源码 + */ +@Slf4j +public class IotBinarySerializer implements IotMessageSerializer { + + /** + * 协议魔术字,用于协议识别 + */ + private static final byte MAGIC_NUMBER = (byte) 0x7E; + + /** + * 协议版本号 + */ + private static final byte PROTOCOL_VERSION = (byte) 0x01; + + /** + * 请求消息类型 + */ + private static final byte REQUEST = (byte) 0x01; + + /** + * 响应消息类型 + */ + private static final byte RESPONSE = (byte) 0x02; + + /** + * 协议头部固定长度(魔术字 + 版本号 + 消息类型 + 消息长度) + */ + private static final int HEADER_FIXED_LENGTH = 7; + + /** + * 最小消息长度(头部 + 消息ID长度 + 方法名长度) + */ + private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4; + + @Override + public IotSerializeTypeEnum getType() { + return IotSerializeTypeEnum.BINARY; + } + + @Override + public byte[] serialize(IotDeviceMessage message) { + Assert.notNull(message, "消息不能为空"); + Assert.notBlank(message.getMethod(), "消息方法不能为空"); + try { + // 1. 确定消息类型 + byte messageType = determineMessageType(message); + // 2. 构建消息体 + byte[] bodyData = buildMessageBody(message, messageType); + // 3. 构建完整消息 + return buildCompleteMessage(message, messageType, bodyData); + } catch (Exception e) { + log.error("[encode][二进制消息编码失败,消息: {}]", message, e); + throw new RuntimeException("二进制消息编码失败: " + e.getMessage(), e); + } + } + + @Override + public IotDeviceMessage deserialize(byte[] bytes) { + Assert.notNull(bytes, "待解码数据不能为空"); + Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足"); + try { + Buffer buffer = Buffer.buffer(bytes); + int index = 0; + + // 1. 验证魔术字 + byte magic = buffer.getByte(index++); + Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic); + + // 2. 验证版本号 + byte version = buffer.getByte(index++); + Assert.isTrue(version == PROTOCOL_VERSION, "不支持的协议版本: " + version); + + // 3. 读取消息类型 + byte messageType = buffer.getByte(index++); + Assert.isTrue(messageType == REQUEST || messageType == RESPONSE, "无效的消息类型: " + messageType); + + // 4. 读取消息长度 + int messageLength = buffer.getInt(index); + index += 4; + Assert.isTrue(messageLength == buffer.length(), + "消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length()); + + // 5. 读取消息 ID + short messageIdLength = buffer.getShort(index); + index += 2; + String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name()); + index += messageIdLength; + + // 6. 读取方法名 + short methodLength = buffer.getShort(index); + index += 2; + String method = buffer.getString(index, index + methodLength, StandardCharsets.UTF_8.name()); + index += methodLength; + + // 7. 解析消息体 + return parseMessageBody(buffer, index, messageType, messageId, method); + } catch (Exception e) { + log.error("[decode][二进制消息解码失败,数据长度: {}]", bytes.length, e); + throw new RuntimeException("二进制消息解码失败: " + e.getMessage(), e); + } + } + + /** + * 快速检测是否为二进制格式 + * + * @param data 数据 + * @return 是否为二进制格式 + */ + public static boolean isBinaryFormat(byte[] data) { + return data != null && data.length >= 1 && data[0] == MAGIC_NUMBER; + } + + private byte determineMessageType(IotDeviceMessage message) { + if (message.getCode() != null) { + return RESPONSE; + } + return REQUEST; + } + + private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) { + Buffer bodyBuffer = Buffer.buffer(); + if (messageType == RESPONSE) { + // code + bodyBuffer.appendInt(message.getCode() != null ? message.getCode() : 0); + // msg + String msg = message.getMsg() != null ? message.getMsg() : ""; + byte[] msgBytes = StrUtil.utf8Bytes(msg); + bodyBuffer.appendShort((short) msgBytes.length); + bodyBuffer.appendBytes(msgBytes); + // data + if (message.getData() != null) { + bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getData())); + } + } else { + // 请求消息只处理 params 参数 + if (message.getParams() != null) { + bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getParams())); + } + } + return bodyBuffer.getBytes(); + } + + private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) { + Buffer buffer = Buffer.buffer(); + // 1. 写入协议头部 + buffer.appendByte(MAGIC_NUMBER); + buffer.appendByte(PROTOCOL_VERSION); + buffer.appendByte(messageType); + // 2. 预留消息长度位置 + int lengthPosition = buffer.length(); + buffer.appendInt(0); + // 3. 写入消息 ID + String messageId = StrUtil.isNotBlank(message.getRequestId()) ? message.getRequestId() + : IotDeviceMessageUtils.generateMessageId(); + byte[] messageIdBytes = StrUtil.utf8Bytes(messageId); + buffer.appendShort((short) messageIdBytes.length); + buffer.appendBytes(messageIdBytes); + // 4. 写入方法名 + byte[] methodBytes = StrUtil.utf8Bytes(message.getMethod()); + buffer.appendShort((short) methodBytes.length); + buffer.appendBytes(methodBytes); + // 5. 写入消息体 + buffer.appendBytes(bodyData); + // 6. 更新消息长度 + buffer.setInt(lengthPosition, buffer.length()); + return buffer.getBytes(); + } + + private IotDeviceMessage parseMessageBody(Buffer buffer, int startIndex, byte messageType, + String messageId, String method) { + if (startIndex >= buffer.length()) { + return IotDeviceMessage.of(messageId, method, null, null, null, null); + } + + if (messageType == RESPONSE) { + return parseResponseMessage(buffer, startIndex, messageId, method); + } else { + Object payload = parseJsonData(buffer, startIndex, buffer.length()); + return IotDeviceMessage.of(messageId, method, payload, null, null, null); + } + } + + private IotDeviceMessage parseResponseMessage(Buffer buffer, int startIndex, String messageId, String method) { + int index = startIndex; + + // 1. 读取响应码 + Integer code = buffer.getInt(index); + index += 4; + + // 2. 读取响应消息 + short msgLength = buffer.getShort(index); + index += 2; + String msg = msgLength > 0 ? buffer.getString(index, index + msgLength, StandardCharsets.UTF_8.name()) : null; + index += msgLength; + + // 3. 读取响应数据 + Object data = null; + if (index < buffer.length()) { + data = parseJsonData(buffer, index, buffer.length()); + } + + return IotDeviceMessage.of(messageId, method, null, data, code, msg); + } + + private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) { + if (startIndex >= endIndex) { + return null; + } + try { + String jsonStr = buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); + return JsonUtils.parseObject(jsonStr, Object.class); + } catch (Exception e) { + log.warn("[parseJsonData][JSON 解析失败,返回原始字符串]", e); + return buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/json/IotJsonSerializer.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/json/IotJsonSerializer.java new file mode 100644 index 0000000000..7fa657075c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/json/IotJsonSerializer.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.gateway.serialize.json; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; + +/** + * JSON 格式的消息序列化器 + * + * 直接使用 JsonUtils 序列化/反序列化 {@link IotDeviceMessage},不包装额外字段 + * + * @author 芋道源码 + */ +public class IotJsonSerializer implements IotMessageSerializer { + + @Override + public IotSerializeTypeEnum getType() { + return IotSerializeTypeEnum.JSON; + } + + @Override + public byte[] serialize(IotDeviceMessage message) { + Assert.notNull(message, "消息不能为空"); + return JsonUtils.toJsonByte(message); + } + + @Override + public IotDeviceMessage deserialize(byte[] bytes) { + Assert.notNull(bytes, "待解码数据不能为空"); + IotDeviceMessage message = JsonUtils.parseObject(bytes, IotDeviceMessage.class); + Assert.notNull(message, "消息解码失败"); + return message; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 691e5cf56c..7ec5c2a463 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -42,14 +42,20 @@ yudao: secret: yudaoIotGatewayTokenSecret123456789 # Token 密钥,至少32位 expiration: 7d - # 协议配置 - protocol: + # 协议实例列表(新版配置方式) + protocols: # ==================================== # 针对引入的 HTTP 组件的配置 # ==================================== - http: + - id: http-json + type: http + port: 8092 enabled: true - server-port: 8092 + http: + ssl-enabled: false + + # 协议配置(旧版,保持兼容) + protocol: # ==================================== # 针对引入的 EMQX 组件的配置 # ==================================== From 44b1950e4a4825bbe8488d257ee0cb99cd568ea2 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 02:52:58 +0800 Subject: [PATCH 08/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91tcp=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=8850%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/IotGatewayConfiguration.java | 44 +--- .../gateway/config/IotGatewayProperties.java | 160 +------------ .../gateway/protocol/IotProtocolManager.java | 19 +- .../protocol/http/IotHttpProtocol.java | 22 +- .../gateway/protocol/tcp/IotTcpConfig.java | 114 ++++++++++ .../gateway/protocol/tcp/IotTcpProtocol.java | 205 +++++++++++++++++ .../protocol/tcp/IotTcpUpstreamProtocol.java | 125 ----------- .../tcp/codec/IotTcpCodecTypeEnum.java | 77 +++++++ .../protocol/tcp/codec/IotTcpFrameCodec.java | 64 ++++++ .../delimiter/IotTcpDelimiterFrameCodec.java | 107 +++++++++ .../length/IotTcpFixedLengthFrameCodec.java | 58 +++++ .../length/IotTcpLengthFieldFrameCodec.java | 166 ++++++++++++++ .../downstream}/IotTcpDownstreamHandler.java | 35 ++- .../IotTcpDownstreamSubscriber.java | 6 +- .../upstream}/IotTcpUpstreamHandler.java | 210 +++++++++--------- .../tcp/manager/IotTcpConnectionManager.java | 5 +- .../src/main/resources/application.yaml | 29 ++- ...irectDeviceTcpProtocolIntegrationTest.java | 116 ++++++---- .../tcp/tcp-binary-packet-examples.md | 193 ---------------- .../protocol/tcp/tcp-json-packet-examples.md | 191 ---------------- 20 files changed, 1039 insertions(+), 907 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/{router => handler/downstream}/IotTcpDownstreamHandler.java (60%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/{ => handler/downstream}/IotTcpDownstreamSubscriber.java (80%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/{router => handler/upstream}/IotTcpUpstreamHandler.java (77%) delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-binary-packet-examples.md delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-json-packet-examples.md diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index d300fd4a77..3aaf3b1d2a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.config; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; -import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; @@ -12,10 +11,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; @@ -24,6 +19,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownst import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; @@ -84,44 +80,6 @@ public class IotGatewayConfiguration { } } - /** - * IoT 网关 TCP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.tcp", name = "enabled", havingValue = "true") - @Slf4j - public static class TcpProtocolConfiguration { - - @Bean(name = "tcpVertx", destroyMethod = "close") - public Vertx tcpVertx() { - return Vertx.vertx(); - } - - @Bean - public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotTcpConnectionManager connectionManager, - @Qualifier("tcpVertx") Vertx tcpVertx) { - return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), - deviceService, messageService, connectionManager, tcpVertx); - } - - @Bean - public IotTcpDownstreamHandler iotTcpDownstreamHandler(IotDeviceMessageService messageService, - IotTcpConnectionManager connectionManager) { - return new IotTcpDownstreamHandler(messageService, connectionManager); - } - - @Bean - public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, - IotTcpDownstreamHandler downstreamHandler, - IotMessageBus messageBus) { - return new IotTcpDownstreamSubscriber(protocolHandler, downstreamHandler, messageBus); - } - - } - /** * IoT 网关 MQTT 协议配置类 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 907a0ae8d9..707c8e37e0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -2,6 +2,9 @@ package cn.iocoder.yudao.module.iot.gateway.config; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import io.vertx.core.net.KeyCertOptions; +import io.vertx.core.net.TrustOptions; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -76,21 +79,11 @@ public class IotGatewayProperties { @Data public static class ProtocolProperties { - /** - * HTTP 组件配置 - */ - private HttpProperties http; - /** * EMQX 组件配置 */ private EmqxProperties emqx; - /** - * TCP 组件配置 - */ - private TcpProperties tcp; - /** * MQTT 组件配置 */ @@ -113,36 +106,6 @@ public class IotGatewayProperties { } - @Data - public static class HttpProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - /** - * 服务端口 - */ - private Integer serverPort; - - /** - * 是否开启 SSL - */ - @NotNull(message = "是否开启 SSL 不能为空") - private Boolean sslEnabled = false; - - /** - * SSL 证书路径 - */ - private String sslKeyPath; - /** - * SSL 证书路径 - */ - private String sslCertPath; - - } - @Data public static class EmqxProperties { @@ -312,47 +275,6 @@ public class IotGatewayProperties { } - @Data - public static class TcpProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务器端口 - */ - private Integer port = 8091; - - /** - * 心跳超时时间(毫秒) - */ - private Long keepAliveTimeoutMs = 30000L; - - /** - * 最大连接数 - */ - private Integer maxConnections = 1000; - - /** - * 是否启用SSL - */ - private Boolean sslEnabled = false; - - /** - * SSL证书路径 - */ - private String sslCertPath; - - /** - * SSL私钥路径 - */ - private String sslKeyPath; - - } - @Data public static class MqttProperties { @@ -381,6 +303,7 @@ public class IotGatewayProperties { */ private Integer keepAliveTimeoutSeconds = 300; + // TODO @AI:所有跟 ssl 相关的参数,是不是可以统一?放到 protocol 层级?ProtocolInstanceProperties【优先级:低】暂时不用规划; /** * 是否启用 SSL */ @@ -399,11 +322,11 @@ public class IotGatewayProperties { /** * 密钥证书选项 */ - private io.vertx.core.net.KeyCertOptions keyCertOptions; + private KeyCertOptions keyCertOptions; /** * 信任选项 */ - private io.vertx.core.net.TrustOptions trustOptions; + private TrustOptions trustOptions; /** * SSL 证书路径 */ @@ -596,78 +519,11 @@ public class IotGatewayProperties { @Valid private IotHttpConfig http; - // TODO @AI:后续改下; /** - * TCP 协议配置(后续扩展) + * TCP 协议配置 */ @Valid - private TcpInstanceConfig tcp; - - } - - /** - * TCP 协议实例配置(后续扩展) - */ - @Data - public static class TcpInstanceConfig { - - /** - * 最大连接数 - */ - private Integer maxConnections = 1000; - - /** - * 心跳超时时间(毫秒) - */ - private Long keepAliveTimeoutMs = 30000L; - - /** - * 是否启用 SSL - */ - private Boolean sslEnabled = false; - - /** - * SSL 证书路径 - */ - private String sslCertPath; - - /** - * SSL 私钥路径 - */ - private String sslKeyPath; - - /** - * 拆包配置 - */ - private CodecConfig codec; - - /** - * TCP 拆包配置 - */ - @Data - public static class CodecConfig { - - /** - * 拆包类型:LENGTH_FIELD / DELIMITER - */ - private String type; - - /** - * LENGTH_FIELD: 偏移量 - */ - private Integer lengthFieldOffset; - - /** - * LENGTH_FIELD: 长度字段长度 - */ - private Integer lengthFieldLength; - - /** - * DELIMITER: 分隔符 - */ - private String delimiter; - - } + private IotTcpConfig tcp; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index 2e339e4a73..c64c828f8d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; import lombok.extern.slf4j.Slf4j; import org.springframework.context.SmartLifecycle; @@ -14,9 +15,7 @@ import java.util.ArrayList; import java.util.List; /** - * IoT 协议管理器 - * - * 负责根据配置创建和管理协议实例 + * IoT 协议管理器:负责根据配置创建和管理协议实例 * * @author 芋道源码 */ @@ -96,7 +95,7 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return 协议实例 */ - @SuppressWarnings({"SwitchStatementWithTooFewBranches", "EnhancedSwitchMigration"}) + @SuppressWarnings({"EnhancedSwitchMigration"}) private IotProtocol createProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getType()); if (protocolType == null) { @@ -106,6 +105,8 @@ public class IotProtocolManager implements SmartLifecycle { switch (protocolType) { case HTTP: return createHttpProtocol(config); + case TCP: + return createTcpProtocol(config); // TODO 后续添加其他协议类型 default: throw new IllegalArgumentException(String.format( @@ -123,4 +124,14 @@ public class IotProtocolManager implements SmartLifecycle { return new IotHttpProtocol(config, messageBus); } + /** + * 创建 TCP 协议实例 + * + * @param config 协议实例配置 + * @return TCP 协议实例 + */ + private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + return new IotTcpProtocol(config, messageBus, serializerManager); + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java index c65e8d87ab..b141afc5e4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java @@ -33,10 +33,6 @@ public class IotHttpProtocol implements IotProtocol { * 协议配置 */ private final ProtocolInstanceProperties properties; - /** - * 消息总线 - */ - private final IotMessageBus messageBus; /** * 服务器 ID(用于消息追踪,全局唯一) */ @@ -44,26 +40,26 @@ public class IotHttpProtocol implements IotProtocol { private final String serverId; /** - * Vert.x 实例(每个 Protocol 自己管理) + * 运行状态 + */ + private volatile boolean running = false; + + /** + * Vert.x 实例 */ private Vertx vertx; /** * HTTP 服务器 */ private HttpServer httpServer; + /** * 下行消息订阅者 */ private IotHttpDownstreamSubscriber downstreamSubscriber; - /** - * 运行状态 - */ - private volatile boolean running = false; - public IotHttpProtocol(ProtocolInstanceProperties properties, IotMessageBus messageBus) { this.properties = properties; - this.messageBus = messageBus; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); } @@ -121,7 +117,6 @@ public class IotHttpProtocol implements IotProtocol { getId(), properties.getPort(), serverId); // 2. 启动下行消息订阅者 - this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); @@ -132,9 +127,6 @@ public class IotHttpProtocol implements IotProtocol { } throw e; } - - // 2. 启动下行消息订阅者 - this.downstreamSubscriber.start(); } @Override diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java new file mode 100644 index 0000000000..c967ce2764 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java @@ -0,0 +1,114 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; + +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT TCP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotTcpConfig { + + /** + * 最大连接数 + */ + @NotNull(message = "最大连接数不能为空") + @Min(value = 1, message = "最大连接数必须大于 0") + private Integer maxConnections = 1000; + /** + * 心跳超时时间(毫秒) + */ + @NotNull(message = "心跳超时时间不能为空") + @Min(value = 1000, message = "心跳超时时间必须大于 1000 毫秒") + private Long keepAliveTimeoutMs = 30000L; + + /** + * 是否启用 SSL + */ + @NotNull(message = "是否启用 SSL 不能为空") + private Boolean sslEnabled = false; + /** + * SSL 证书路径 + */ + private String sslCertPath; + /** + * SSL 私钥路径 + */ + private String sslKeyPath; + + /** + * 拆包配置 + */ + @Valid + private CodecConfig codec; + + /** + * TCP 拆包配置 + */ + @Data + public static class CodecConfig { + + /** + * 拆包类型 + * + * @see IotTcpCodecTypeEnum + */ + @NotNull(message = "拆包类型不能为空") + private String type; + + /** + * LENGTH_FIELD: 长度字段偏移量 + *

+ * 表示长度字段在消息中的起始位置(从0开始) + */ + private Integer lengthFieldOffset; + /** + * LENGTH_FIELD: 长度字段长度(字节数) + *

+ * 常见值:1(最大255)、2(最大65535)、4(最大2GB) + */ + private Integer lengthFieldLength; + /** + * LENGTH_FIELD: 长度调整值 + *

+ * 用于调整长度字段的值,例如长度字段包含头部长度时需要减去头部长度 + */ + private Integer lengthAdjustment = 0; + /** + * LENGTH_FIELD: 跳过的初始字节数 + *

+ * 解码后跳过的字节数,通常等于 lengthFieldOffset + lengthFieldLength + */ + private Integer initialBytesToStrip = 0; + + /** + * DELIMITER: 分隔符 + *

+ * 支持转义字符:\n(换行)、\r(回车)、\r\n(回车换行) + */ + private String delimiter; + + /** + * FIXED_LENGTH: 固定消息长度(字节) + *

+ * 每条消息的固定长度 + */ + private Integer fixedLength; + + /** + * 最大帧长度(字节) + *

+ * 防止内存溢出,默认 1MB + */ + @NotNull(message = "最大帧长度不能为空") + @Min(value = 1, message = "最大帧长度必须大于 0") + private Integer maxFrameLength = 1048576; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java new file mode 100644 index 0000000000..c03383a224 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java @@ -0,0 +1,205 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream.IotTcpUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +import io.vertx.core.net.NetServer; +import io.vertx.core.net.NetServerOptions; +import io.vertx.core.net.PemKeyCertOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT TCP 协议实现 + *

+ * 基于 Vert.x 实现 TCP 服务器,接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolInstanceProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * TCP 服务器 + */ + private NetServer tcpServer; + + /** + * 下行消息订阅者 + */ + private final IotTcpDownstreamSubscriber downstreamSubscriber; + + /** + * 消息序列化器 + */ + private final IotMessageSerializer serializer; + + /** + * TCP 帧编解码器 + */ + private final IotTcpFrameCodec frameCodec; + + public IotTcpProtocol(ProtocolInstanceProperties properties, IotMessageBus messageBus, + IotMessageSerializerManager serializerManager) { + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); + if (serializeType == null) { + serializeType = IotSerializeTypeEnum.JSON; // 默认 JSON + } + this.serializer = serializerManager.get(serializeType); + + // 初始化帧编解码器 + IotTcpConfig tcpConfig = properties.getTcp(); + IotTcpConfig.CodecConfig codecConfig = tcpConfig != null ? tcpConfig.getCodec() : null; + this.frameCodec = IotTcpFrameCodec.create(codecConfig); + + // 初始化下行消息订阅者 + IotTcpConnectionManager connectionManager = SpringUtil.getBean(IotTcpConnectionManager.class); + IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); + this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.TCP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT TCP 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例(每个 Protocol 独立管理) + this.vertx = Vertx.vertx(); + + // 1.2 创建服务器选项 + IotTcpConfig tcpConfig = properties.getTcp(); + NetServerOptions options = new NetServerOptions() + .setPort(properties.getPort()) + .setTcpKeepAlive(true) + .setTcpNoDelay(true) + .setReuseAddress(true); + if (tcpConfig != null && Boolean.TRUE.equals(tcpConfig.getSslEnabled())) { + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(tcpConfig.getSslKeyPath()) + .setCertPath(tcpConfig.getSslCertPath()); + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + + // 1.3 创建服务器并设置连接处理器 + tcpServer = vertx.createNetServer(options); + IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); + IotTcpConnectionManager connectionManager = SpringUtil.getBean(IotTcpConnectionManager.class); + tcpServer.connectHandler(socket -> { + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, + connectionManager, frameCodec, serializer); + handler.handle(socket); + }); + + // 1.4 启动 TCP 服务器 + try { + tcpServer.listen().result(); + running = true; + log.info("[start][IoT TCP 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT TCP 协议 {} 启动失败]", getId(), e); + // 启动失败时关闭 Vertx + if (vertx != null) { + vertx.close(); + vertx = null; + } + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + // 1. 停止下行消息订阅者 + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT TCP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + + // 2.1 关闭 TCP 服务器 + if (tcpServer != null) { + try { + tcpServer.close().result(); + log.info("[stop][IoT TCP 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT TCP 协议 {} 服务器停止失败]", getId(), e); + } + tcpServer = null; + } + // 2.2 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT TCP 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT TCP 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT TCP 协议 {} 已停止]", getId()); + } + + @Override + public boolean isRunning() { + return running; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java deleted file mode 100644 index e5aeb78c08..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java +++ /dev/null @@ -1,125 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; - -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.core.net.NetServer; -import io.vertx.core.net.NetServerOptions; -import io.vertx.core.net.PemKeyCertOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 TCP 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotTcpUpstreamProtocol implements IotProtocol { - - private static final String ID = "tcp"; - - private final IotGatewayProperties.TcpProperties tcpProperties; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotTcpConnectionManager connectionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private NetServer tcpServer; - - private volatile boolean running = false; - - public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotTcpConnectionManager connectionManager, - Vertx vertx) { - this.tcpProperties = tcpProperties; - this.deviceService = deviceService; - this.messageService = messageService; - this.connectionManager = connectionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); - } - - @Override - public String getId() { - return ID; - } - - @Override - public IotProtocolTypeEnum getType() { - return IotProtocolTypeEnum.TCP; - } - - @Override - @PostConstruct - public void start() { - // 创建服务器选项 - NetServerOptions options = new NetServerOptions() - .setPort(tcpProperties.getPort()) - .setTcpKeepAlive(true) - .setTcpNoDelay(true) - .setReuseAddress(true); - // 配置 SSL(如果启用) - if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) { - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(tcpProperties.getSslKeyPath()) - .setCertPath(tcpProperties.getSslCertPath()); - options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); - } - - // 创建服务器并设置连接处理器 - tcpServer = vertx.createNetServer(options); - tcpServer.connectHandler(socket -> { - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, - connectionManager); - handler.handle(socket); - }); - - // 启动服务器 - try { - tcpServer.listen().result(); - running = true; - log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 TCP 协议启动失败]", e); - throw e; - } - } - - @Override - @PreDestroy - public void stop() { - if (tcpServer != null) { - try { - tcpServer.close().result(); - running = false; - log.info("[stop][IoT 网关 TCP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 TCP 协议停止失败]", e); - } - } - } - - @Override - public boolean isRunning() { - return running; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java new file mode 100644 index 0000000000..7b4b669112 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter.IotTcpDelimiterFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpFixedLengthFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpLengthFieldFrameCodec; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.function.Function; + +/** + * IoT TCP 拆包类型枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum IotTcpCodecTypeEnum { + + /** + * 基于固定长度的拆包 + *

+ * 消息格式:固定长度的消息体 + * 需要配置:fixedLength(固定长度) + */ + FIXED_LENGTH("fixed_length", IotTcpFixedLengthFrameCodec::new), + + /** + * 基于分隔符的拆包 + *

+ * 消息格式:消息内容 + 分隔符 + * 需要配置:delimiter(分隔符) + */ + DELIMITER("delimiter", IotTcpDelimiterFrameCodec::new), + + /** + * 基于长度字段的拆包 + *

+ * 消息格式:[长度字段][消息体] + * 需要配置:lengthFieldOffset(长度字段偏移量)、lengthFieldLength(长度字段长度) + */ + LENGTH_FIELD("length_field", IotTcpLengthFieldFrameCodec::new), + ; + + /** + * 类型标识 + */ + private final String type; + + /** + * Codec 创建工厂 + */ + private final Function codecFactory; + + /** + * 根据类型获取枚举 + * + * @param type 类型标识 + * @return 枚举值 + */ + public static IotTcpCodecTypeEnum of(String type) { + return ArrayUtil.firstMatch(e -> e.getType().equalsIgnoreCase(type), values()); + } + + /** + * 创建 Codec 实例 + * + * @param config 拆包配置 + * @return Codec 实例 + */ + public IotTcpFrameCodec createCodec(IotTcpConfig.CodecConfig config) { + return codecFactory.apply(config); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java new file mode 100644 index 0000000000..7ee16d3a0d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; + +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; + +/** + * IoT TCP 帧编解码器接口 + *

+ * 用于解决 TCP 粘包/拆包问题,提供解码(拆包)和编码(加帧)能力 + * + * @author 芋道源码 + */ +public interface IotTcpFrameCodec { + + /** + * 获取编解码器类型 + * + * @return 编解码器类型 + */ + IotTcpCodecTypeEnum getType(); + + /** + * 创建解码器(RecordParser) + *

+ * 每个连接调用一次,返回的 parser 需绑定到 socket.handler() + * + * @param handler 消息处理器,当收到完整的消息帧后回调 + * @return RecordParser 实例 + */ + RecordParser createDecodeParser(Handler handler); + + /** + * 编码消息(加帧) + *

+ * 根据不同的编解码类型添加帧头/分隔符 + * + * @param data 原始数据 + * @return 编码后的数据(带帧头/分隔符) + */ + Buffer encode(byte[] data); + + // TODO @AI:还是搞个 facory 类 ,更好理解; + // ========== 静态工厂方法 ========== + + /** + * 根据配置创建编解码器 + * + * @param config 拆包配置 + * @return 编解码器实例,如果配置为空则返回 null + */ + static IotTcpFrameCodec create(IotTcpConfig.CodecConfig config) { + if (config == null) { + return null; + } + IotTcpCodecTypeEnum type = IotTcpCodecTypeEnum.of(config.getType()); + if (type == null) { + return null; + } + return type.createCodec(config); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java new file mode 100644 index 0000000000..6d4b8b009a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT TCP 分隔符帧编解码器 + *

+ * 基于分隔符的拆包策略,消息格式:消息内容 + 分隔符 + *

+ * 支持的分隔符: + *

    + *
  • \n - 换行符
  • + *
  • \r - 回车符
  • + *
  • \r\n - 回车换行
  • + *
  • 自定义字符串
  • + *
+ * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { + + private final IotTcpConfig.CodecConfig config; + + /** + * 解析后的分隔符字节数组 + */ + private final byte[] delimiterBytes; + + /** + * 最大帧长度 + */ + // TODO @AI:最大帧数要不去掉;简洁一点;包括其他地方的配置项; + private final int maxFrameLength; + + public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) { + this.config = config; + // TODO @AI:禁止为空; + this.delimiterBytes = parseDelimiter(config.getDelimiter()); + this.maxFrameLength = config.getMaxFrameLength() != null ? config.getMaxFrameLength() : 1048576; + } + + @Override + public IotTcpCodecTypeEnum getType() { + return IotTcpCodecTypeEnum.DELIMITER; + } + + @Override + public RecordParser createDecodeParser(Handler handler) { + RecordParser parser = RecordParser.newDelimited(Buffer.buffer(delimiterBytes)); + + parser.handler(buffer -> { + // 检查帧长度是否超过限制 + if (buffer.length() > maxFrameLength) { + log.warn("[createDecodeParser][帧长度超过限制,length: {}, maxFrameLength: {}]", + buffer.length(), maxFrameLength); + return; + } + // 处理完整消息(不包含分隔符) + handler.handle(buffer); + }); + + // TODO @AI:异常处理; + parser.exceptionHandler(ex -> log.error("[createDecodeParser][解析异常]", ex)); + return parser; + } + + @Override + public Buffer encode(byte[] data) { + Buffer buffer = Buffer.buffer(); + buffer.appendBytes(data); + buffer.appendBytes(delimiterBytes); + return buffer; + } + + /** + * 解析分隔符字符串为字节数组 + *

+ * 支持转义字符:\n、\r、\r\n、\t + * + * @param delimiter 分隔符字符串 + * @return 分隔符字节数组 + */ + private byte[] parseDelimiter(String delimiter) { + if (StrUtil.isBlank(delimiter)) { + // 默认使用换行符 + return new byte[]{'\n'}; + } + + // 处理转义字符 + // TODO @AI:是否必要?不调整感觉也没问题?用户自己写对就 ok 了是哇? + String parsed = delimiter + .replace("\\r\\n", "\r\n") + .replace("\\r", "\r") + .replace("\\n", "\n") + .replace("\\t", "\t"); + return parsed.getBytes(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java new file mode 100644 index 0000000000..c9269fc383 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length; + +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT TCP 定长帧编解码器 + *

+ * 基于固定长度的拆包策略,每条消息固定字节数 + * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpFixedLengthFrameCodec implements IotTcpFrameCodec { + + /** + * 固定消息长度 + */ + private final int fixedLength; + + public IotTcpFixedLengthFrameCodec(IotTcpConfig.CodecConfig config) { + // TODO @AI:config.getFixedLength() 禁止为空; + this.fixedLength = config.getFixedLength() != null ? config.getFixedLength() : 1024; + } + + @Override + public IotTcpCodecTypeEnum getType() { + return IotTcpCodecTypeEnum.FIXED_LENGTH; + } + + @Override + public RecordParser createDecodeParser(Handler handler) { + RecordParser parser = RecordParser.newFixed(fixedLength); + parser.handler(handler); + // TODO @AI:解析失败,是不是要抛出异常?因为要 close 掉连接; + parser.exceptionHandler(ex -> log.error("[createDecodeParser][解析异常]", ex)); + return parser; + } + + @Override + public Buffer encode(byte[] data) { + Buffer buffer = Buffer.buffer(fixedLength); + buffer.appendBytes(data); + // 如果数据不足固定长度,填充 0 + // TODO @AI:这里的填充是合理的么?RecordParser.newFixed(fixedLength) 有填充的逻辑么? + if (data.length < fixedLength) { + byte[] padding = new byte[fixedLength - data.length]; + buffer.appendBytes(padding); + } + return buffer; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java new file mode 100644 index 0000000000..12a0b680eb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java @@ -0,0 +1,166 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length; + +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT TCP 长度字段帧编解码器 + *

+ * 基于长度字段的拆包策略,消息格式:[长度字段][消息体] + *

+ * 参数说明: + *

    + *
  • lengthFieldOffset: 长度字段在消息中的偏移量
  • + *
  • lengthFieldLength: 长度字段的字节数(1/2/4)
  • + *
  • lengthAdjustment: 长度调整值,用于调整长度字段的实际含义
  • + *
  • initialBytesToStrip: 解码后跳过的字节数
  • + *
+ * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec { + + private final int lengthFieldOffset; + private final int lengthFieldLength; + private final int lengthAdjustment; + private final int initialBytesToStrip; + // TODO @AI:去掉 maxFrameLength 相关字段; + private final int maxFrameLength; + + /** + * 头部长度 = 长度字段偏移量 + 长度字段长度 + */ + private final int headerLength; + + public IotTcpLengthFieldFrameCodec(IotTcpConfig.CodecConfig config) { + // TODO @AI: 增加参数校验;不要 default 逻辑; + this.lengthFieldOffset = config.getLengthFieldOffset() != null ? config.getLengthFieldOffset() : 0; + this.lengthFieldLength = config.getLengthFieldLength() != null ? config.getLengthFieldLength() : 4; + this.lengthAdjustment = config.getLengthAdjustment() != null ? config.getLengthAdjustment() : 0; + this.initialBytesToStrip = config.getInitialBytesToStrip() != null ? config.getInitialBytesToStrip() : 0; + this.maxFrameLength = config.getMaxFrameLength() != null ? config.getMaxFrameLength() : 1048576; + this.headerLength = lengthFieldOffset + lengthFieldLength; + } + + @Override + public IotTcpCodecTypeEnum getType() { + return IotTcpCodecTypeEnum.LENGTH_FIELD; + } + + @Override + public RecordParser createDecodeParser(Handler handler) { + // 创建状态机:先读取头部,再读取消息体 + RecordParser parser = RecordParser.newFixed(headerLength); + // 使用数组保存状态和头部数据 + // TODO @AI:bodyLength 只使用第 0 位,是不是 atomicInteger 更合适? + final int[] bodyLength = {-1}; + final Buffer[] headerBuffer = {null}; + + // 处理读取到的数据 + parser.handler(buffer -> { + if (bodyLength[0] == -1) { + // 阶段 1: 读取头部,解析长度字段 + headerBuffer[0] = buffer.copy(); + int length = readLength(buffer, lengthFieldOffset, lengthFieldLength); + int frameBodyLength = length + lengthAdjustment; + // 检查帧长度是否超过限制 + if (frameBodyLength < 0 || frameBodyLength > maxFrameLength - headerLength) { + log.warn("[createDecodeParser][帧长度异常,length: {}, frameBodyLength: {}, maxFrameLength: {}]", + length, frameBodyLength, maxFrameLength); + return; + } + + if (frameBodyLength == 0) { + // 消息体为空,直接处理 + // TODO @AI:消息体为空,是不是不合理哈?应该抛出异常? + Buffer frame = processFrame(headerBuffer[0], null); + handler.handle(frame); + } else { + // 切换到读取消息体模式 + bodyLength[0] = frameBodyLength; + parser.fixedSizeMode(frameBodyLength); + } + } else { + // 阶段 2: 读取消息体,组装完整帧 + Buffer frame = processFrame(headerBuffer[0], buffer); + // 重置状态,准备读取下一帧 + bodyLength[0] = -1; + headerBuffer[0] = null; + parser.fixedSizeMode(headerLength); + + // 处理完整消息 + handler.handle(frame); + } + }); + + parser.exceptionHandler(ex -> log.error("[createDecodeParser][解析异常]", ex)); + return parser; + } + + @Override + public Buffer encode(byte[] data) { + Buffer buffer = Buffer.buffer(); + // 计算要写入的长度值 + int lengthValue = data.length - lengthAdjustment; + // 写入偏移量前的填充字节(如果有) + for (int i = 0; i < lengthFieldOffset; i++) { + buffer.appendByte((byte) 0); + } + // 写入长度字段 + writeLength(buffer, lengthValue, lengthFieldLength); + // 写入消息体 + buffer.appendBytes(data); + return buffer; + } + + /** + * 从 Buffer 中读取长度字段 + */ + // TODO @AI:兼容 JDK8 + private int readLength(Buffer buffer, int offset, int length) { + return switch (length) { + case 1 -> buffer.getUnsignedByte(offset); + case 2 -> buffer.getUnsignedShort(offset); + case 4 -> buffer.getInt(offset); + default -> throw new IllegalArgumentException("不支持的长度字段长度: " + length); + }; + } + + /** + * 向 Buffer 中写入长度字段 + */ + // TODO @AI:兼容 JDK8 + private void writeLength(Buffer buffer, int length, int fieldLength) { + switch (fieldLength) { + case 1 -> buffer.appendByte((byte) length); + case 2 -> buffer.appendShort((short) length); + case 4 -> buffer.appendInt(length); + default -> throw new IllegalArgumentException("不支持的长度字段长度: " + fieldLength); + } + } + + /** + * 处理帧数据(根据 initialBytesToStrip 跳过指定字节) + */ + private Buffer processFrame(Buffer header, Buffer body) { + Buffer fullFrame = Buffer.buffer(); + if (header != null) { + fullFrame.appendBuffer(header); + } + if (body != null) { + fullFrame.appendBuffer(body); + } + // 根据 initialBytesToStrip 跳过指定字节 + if (initialBytesToStrip > 0 && initialBytesToStrip < fullFrame.length()) { + return fullFrame.slice(initialBytesToStrip, fullFrame.length()); + } + return fullFrame; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java similarity index 60% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java index 374e75287b..c87eebb39c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java @@ -1,8 +1,10 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import io.vertx.core.buffer.Buffer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -15,10 +17,17 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public class IotTcpDownstreamHandler { - private final IotDeviceMessageService deviceMessageService; - private final IotTcpConnectionManager connectionManager; + /** + * TCP 帧编解码器(处理粘包/拆包) + */ + private final IotTcpFrameCodec codec; + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + /** * 处理下行消息 */ @@ -26,21 +35,25 @@ public class IotTcpDownstreamHandler { try { log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", message.getDeviceId(), message.getMethod(), message.getId()); - - // 1. 获取连接信息(包含 codecType) + // 1. 检查设备连接 IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( message.getDeviceId()); if (connectionInfo == null) { - log.error("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId()); + // TODO @AI:是不是把消息 id 也打印进去?类似上面的日志 + log.warn("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId()); return; } - // 2. 使用连接时的 codecType 编码消息,并发送到设备 - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType()); - boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes); + // 2. 序列化 + 帧编码 + byte[] serializedData = serializer.serialize(message); + Buffer frameData = codec.encode(serializedData); + + // 3. 发送到设备 + boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes()); + // TODO @AI:不成功,直接抛出异常;反正下面的日志也会打印失败的 if (success) { log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); + message.getDeviceId(), message.getMethod(), message.getId(), frameData.length()); } else { log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", message.getDeviceId(), message.getMethod(), message.getId()); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java similarity index 80% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java index f04d2b2381..7a29e6c00c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java @@ -1,9 +1,9 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; import lombok.extern.slf4j.Slf4j; /** @@ -16,7 +16,7 @@ public class IotTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber private final IotTcpDownstreamHandler downstreamHandler; - public IotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocol, + public IotTcpDownstreamSubscriber(IotProtocol protocol, IotTcpDownstreamHandler downstreamHandler, IotMessageBus messageBus) { super(protocol, messageBus); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java similarity index 77% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java index 4a20f46af2..c5b6267eba 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java @@ -1,9 +1,8 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -16,15 +15,16 @@ import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; import java.util.Map; @@ -37,9 +37,6 @@ import java.util.Map; @Slf4j public class IotTcpUpstreamHandler implements Handler { - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; - private static final String AUTH_METHOD = "auth"; private final IotDeviceMessageService deviceMessageService; @@ -52,15 +49,29 @@ public class IotTcpUpstreamHandler implements Handler { private final String serverId; - public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, + /** + * TCP 帧编解码器(处理粘包/拆包) + */ + private final IotTcpFrameCodec codec; + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + + public IotTcpUpstreamHandler(IotProtocol protocol, IotDeviceMessageService deviceMessageService, IotDeviceService deviceService, - IotTcpConnectionManager connectionManager) { + IotTcpConnectionManager connectionManager, + IotTcpFrameCodec codec, + IotMessageSerializer serializer) { + this.serverId = protocol.getServerId(); + this.codec = codec; + this.serializer = serializer; + this.connectionManager = connectionManager; + // TODO @AI:都通过 springutil 获取下; this.deviceMessageService = deviceMessageService; this.deviceService = deviceService; - this.connectionManager = connectionManager; this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.serverId = protocol.getServerId(); } @Override @@ -78,18 +89,32 @@ public class IotTcpUpstreamHandler implements Handler { cleanupConnection(socket); }); - // 设置消息处理器 - socket.handler(buffer -> { - // TODO @AI:TODO @芋艿:这里应该有拆粘包的问题; + // 设置消息处理器(带拆包支持) + Handler messageHandler = buffer -> { + // TODO @AI:需要跟 AI 讨论。哪些情况关闭;哪些情况,发送异常消息; try { processMessage(clientId, buffer, socket); } catch (Exception e) { + // TODO @AI:这里能合并到 exceptionHandler 么?还是怎么搞好点; log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", clientId, socket.remoteAddress(), e.getMessage()); cleanupConnection(socket); socket.close(); } - }); + }; + + // 根据是否配置了 FrameCodec 来决定是否使用拆包器 + // TODO @AI:必须配置! + if (codec != null) { + // 使用拆包器处理粘包/拆包 + RecordParser parser = codec.createDecodeParser(messageHandler); + socket.handler(parser); + log.debug("[handle][启用 {} 拆包器,客户端 ID: {}]", codec.getType(), clientId); + } else { + // 未配置拆包器,直接处理原始数据(可能存在粘包问题) + socket.handler(messageHandler); + log.debug("[handle][未配置拆包器,客户端 ID: {}]", clientId); + } } /** @@ -102,43 +127,42 @@ public class IotTcpUpstreamHandler implements Handler { */ private void processMessage(String clientId, Buffer buffer, NetSocket socket) throws Exception { // 1. 基础检查 + // TODO @AI:不太应该为空?! if (buffer == null || buffer.length() == 0) { return; } - // 2. 获取消息格式类型 - String codecType = getMessageCodecType(buffer, socket); - - // 3. 解码消息 + // 2. 反序列化消息 IotDeviceMessage message; try { - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); + message = serializer.deserialize(buffer.getBytes()); if (message == null) { - throw new Exception("解码后消息为空"); + throw new IllegalArgumentException("反序列化后消息为空"); } } catch (Exception e) { - // 消息格式错误时抛出异常,由上层处理连接断开 - throw new Exception("消息解码失败: " + e.getMessage(), e); + // TODO @AI:是不是不用 try catch? + throw new Exception("消息反序列化失败: " + e.getMessage(), e); } - // 4. 根据消息类型路由处理 + // 3. 根据消息类型路由处理 try { if (AUTH_METHOD.equals(message.getMethod())) { // 认证请求 - handleAuthenticationRequest(clientId, message, codecType, socket); + handleAuthenticationRequest(clientId, message, socket); } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { // 设备动态注册请求 - handleRegisterRequest(clientId, message, codecType, socket); + handleRegisterRequest(clientId, message, socket); } else { // 业务消息 - handleBusinessRequest(clientId, message, codecType, socket); + handleBusinessRequest(clientId, message, socket); } } catch (Exception e) { - log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", - clientId, message.getMethod(), e); + // TODO @AI:如果参数不正确,不断开连接; + log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", clientId, message.getMethod(), e); // 发送错误响应,避免客户端一直等待 + // TODO @AI:发送失败,是不是不用 try catch? try { - sendErrorResponse(socket, message.getRequestId(), "消息处理失败", codecType); + sendErrorResponse(socket, message.getRequestId(), "消息处理失败"); } catch (Exception responseEx) { log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx); } @@ -148,74 +172,73 @@ public class IotTcpUpstreamHandler implements Handler { /** * 处理认证请求 * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param socket 网络连接 + * @param clientId 客户端 ID + * @param message 消息信息 + * @param socket 网络连接 */ - private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, String codecType, - NetSocket socket) { + private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, NetSocket socket) { try { // 1.1 解析认证参数 + // TODO @AI:直接 JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);然后,校验参数,不正确抛出 invalid exception;和 http 那一样; IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); if (authParams == null) { log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "认证参数不完整", codecType); + sendErrorResponse(socket, message.getRequestId(), "认证参数不完整"); return; } // 1.2 执行认证 if (!validateDeviceAuth(authParams)) { log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", clientId, authParams.getUsername()); - sendErrorResponse(socket, message.getRequestId(), "认证失败", codecType); + sendErrorResponse(socket, message.getRequestId(), "认证失败"); return; } // 2.1 解析设备信息 IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); + // TODO @AI:这里就断言 deviceInfo 不为空了?! if (deviceInfo == null) { - sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType); + sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败"); return; } // 2.2 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + // TODO @AI:这里就断言 device 不为空了?! if (device == null) { - sendErrorResponse(socket, message.getRequestId(), "设备不存在", codecType); + sendErrorResponse(socket, message.getRequestId(), "设备不存在"); return; } // 3.1 注册连接 - registerConnection(socket, device, clientId, codecType); + registerConnection(socket, device, clientId); // 3.2 发送上线消息 sendOnlineMessage(device); // 3.3 发送成功响应 - sendSuccessResponse(socket, message.getRequestId(), "认证成功", codecType); + sendSuccessResponse(socket, message.getRequestId(), "认证成功"); log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", device.getId(), device.getDeviceName()); } catch (Exception e) { + // TODO @AI:最大化去掉 try catch;(这个方法里的) log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "认证处理异常", codecType); + sendErrorResponse(socket, message.getRequestId(), "认证处理异常"); } } /** * 处理设备动态注册请求(一型一密,不需要认证) * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param socket 网络连接 + * @param clientId 客户端 ID + * @param message 消息信息 + * @param socket 网络连接 * @see 阿里云 - 一型一密 */ - private void handleRegisterRequest(String clientId, IotDeviceMessage message, String codecType, - NetSocket socket) { + private void handleRegisterRequest(String clientId, IotDeviceMessage message, NetSocket socket) { try { // 1. 解析注册参数 IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); if (params == null) { log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "注册参数不完整", codecType); + sendErrorResponse(socket, message.getRequestId(), "注册参数不完整"); return; } @@ -223,34 +246,33 @@ public class IotTcpUpstreamHandler implements Handler { CommonResult result = deviceApi.registerDevice(params); if (result.isError()) { log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendErrorResponse(socket, message.getRequestId(), result.getMsg(), codecType); + sendErrorResponse(socket, message.getRequestId(), result.getMsg()); return; } // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData(), codecType); + sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData()); log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", clientId, params.getDeviceName()); } catch (Exception e) { log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "注册处理异常", codecType); + sendErrorResponse(socket, message.getRequestId(), "注册处理异常"); } } /** * 处理业务请求 * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param socket 网络连接 + * @param clientId 客户端 ID + * @param message 消息信息 + * @param socket 网络连接 */ - private void handleBusinessRequest(String clientId, IotDeviceMessage message, String codecType, NetSocket socket) { + private void handleBusinessRequest(String clientId, IotDeviceMessage message, NetSocket socket) { try { // 1. 检查认证状态 if (connectionManager.isNotAuthenticated(socket)) { log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "请先进行认证", codecType); + sendErrorResponse(socket, message.getRequestId(), "请先进行认证"); return; } @@ -267,42 +289,19 @@ public class IotTcpUpstreamHandler implements Handler { } } - /** - * 获取消息编解码类型 - * - * @param buffer 消息 - * @param socket 网络连接 - * @return 消息编解码类型 - */ - private String getMessageCodecType(Buffer buffer, NetSocket socket) { - // 1. 如果已认证,优先使用缓存的编解码类型 - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo != null - && StrUtil.isNotBlank(connectionInfo.getCodecType())) { - return connectionInfo.getCodecType(); - } - - // 2. 未认证时检测消息格式类型 - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY - : CODEC_TYPE_JSON; - } - /** * 注册连接信息 * - * @param socket 网络连接 - * @param device 设备 - * @param clientId 客户端 ID - * @param codecType 消息编解码类型 + * @param socket 网络连接 + * @param device 设备 + * @param clientId 客户端 ID */ - private void registerConnection(NetSocket socket, IotDeviceRespDTO device, - String clientId, String codecType) { + private void registerConnection(NetSocket socket, IotDeviceRespDTO device, String clientId) { IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo() .setDeviceId(device.getId()) .setProductKey(device.getProductKey()) .setDeviceName(device.getDeviceName()) - .setClientId(clientId) - .setCodecType(codecType); + .setClientId(clientId); // 注册连接 connectionManager.registerConnection(socket, device.getId(), connectionInfo); } @@ -351,10 +350,10 @@ public class IotTcpUpstreamHandler implements Handler { * @param success 是否成功 * @param message 消息 * @param requestId 请求 ID - * @param codecType 消息编解码类型 */ - private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) { + private void sendResponse(NetSocket socket, boolean success, String message, String requestId) { try { + // TODO @AI:是不是不用 Object responseData = MapUtil.builder() .put("success", success) .put("message", message) @@ -364,14 +363,17 @@ public class IotTcpUpstreamHandler implements Handler { IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, code, message); - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.write(Buffer.buffer(encodedData)); + // 序列化 + 帧编码 + byte[] serializedData = serializer.serialize(responseMessage); + Buffer frameData = codec.encode(serializedData); + socket.write(frameData); } catch (Exception e) { log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); } } + // TODO @AI:合并到 handleAuthenticationRequest 里; /** * 验证设备认证信息 * @@ -397,10 +399,9 @@ public class IotTcpUpstreamHandler implements Handler { * @param socket 网络连接 * @param requestId 请求 ID * @param errorMessage 错误消息 - * @param codecType 消息编解码类型 */ - private void sendErrorResponse(NetSocket socket, String requestId, String errorMessage, String codecType) { - sendResponse(socket, false, errorMessage, requestId, codecType); + private void sendErrorResponse(NetSocket socket, String requestId, String errorMessage) { + sendResponse(socket, false, errorMessage, requestId); } /** @@ -409,11 +410,10 @@ public class IotTcpUpstreamHandler implements Handler { * @param socket 网络连接 * @param requestId 请求 ID * @param message 消息 - * @param codecType 消息编解码类型 */ @SuppressWarnings("SameParameterValue") - private void sendSuccessResponse(NetSocket socket, String requestId, String message, String codecType) { - sendResponse(socket, true, message, requestId, codecType); + private void sendSuccessResponse(NetSocket socket, String requestId, String message) { + sendResponse(socket, true, message, requestId); } /** @@ -489,17 +489,17 @@ public class IotTcpUpstreamHandler implements Handler { * @param socket 网络连接 * @param requestId 请求 ID * @param registerResp 注册响应 - * @param codecType 消息编解码类型 */ private void sendRegisterSuccessResponse(NetSocket socket, String requestId, - IotDeviceRegisterRespDTO registerResp, String codecType) { + IotDeviceRegisterRespDTO registerResp) { try { // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - // 2. 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.write(Buffer.buffer(encodedData)); + // 2. 序列化 + 帧编码 + byte[] serializedData = serializer.serialize(responseMessage); + Buffer frameData = codec.encode(serializedData); + socket.write(frameData); } catch (Exception e) { log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java index c0f2cf7aaa..e236a6db9f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java @@ -81,6 +81,7 @@ public class IotTcpConnectionManager { return info != null; } + // TODO @AI:是不是可以去掉;因为现在只有认证成功的,才会注册连接; /** * 检查连接是否未认证 */ @@ -148,10 +149,6 @@ public class IotTcpConnectionManager { * 客户端 ID */ private String clientId; - /** - * 消息编解码类型(认证后确定) - */ - private String codecType; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 7ec5c2a463..16ae6298d3 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -42,7 +42,7 @@ yudao: secret: yudaoIotGatewayTokenSecret123456789 # Token 密钥,至少32位 expiration: 7d - # 协议实例列表(新版配置方式) + # 协议实例列表 protocols: # ==================================== # 针对引入的 HTTP 组件的配置 @@ -53,6 +53,22 @@ yudao: enabled: true http: ssl-enabled: false + # ==================================== + # 针对引入的 TCP 组件的配置 + # ==================================== + - id: tcp-json + type: tcp + port: 8091 + enabled: true + serialize: json + tcp: + max-connections: 1000 + keep-alive-timeout-ms: 30000 + ssl-enabled: false + codec: + type: delimiter # 拆包类型:length_field / delimiter / fixed_length + delimiter: "\\n" # 分隔符(支持转义:\\n=换行, \\r=回车, \\t=制表符) + max-frame-length: 1048576 # 最大帧长度(字节) # 协议配置(旧版,保持兼容) protocol: @@ -91,17 +107,6 @@ yudao: trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 # ==================================== - # 针对引入的 TCP 组件的配置 - # ==================================== - tcp: - enabled: false - port: 8091 - keep-alive-timeout-ms: 30000 - max-connections: 1000 - ssl-enabled: false - ssl-cert-path: "classpath:certs/client.jks" - ssl-key-path: "classpath:certs/client.jks" - # ==================================== # 针对引入的 UDP 组件的配置 # ==================================== udp: diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java index 4b6936c63c..b386cd1455 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java @@ -10,32 +10,40 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.binary.IotBinarySerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.InputStream; +import java.io.BufferedReader; +import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; +import java.nio.charset.StandardCharsets; /** * IoT 直连设备 TCP 协议集成测试(手动测试) * *

测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 TCP 协议直接连接平台 * - *

支持两种编解码格式: + *

支持两种序列化格式: *

    - *
  • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
  • - *
  • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
  • + *
  • {@link IotJsonSerializer} - JSON 格式
  • + *
  • {@link IotBinarySerializer} - 二进制格式
  • + *
+ * + *

TCP 拆包配置(需与 application.yaml 中的 codec 配置一致): + *

    + *
  • type: delimiter - 基于分隔符拆包
  • + *
  • delimiter: \n - 换行符作为分隔符
  • *
* *

使用步骤: *

    *
  1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
  2. - *
  3. 修改 {@link #CODEC} 选择测试的编解码格式
  4. + *
  5. 修改 {@link #SERIALIZER} 选择测试的序列化格式(Delimiter 模式只支持 JSON)
  6. *
  7. 运行以下测试方法: *
      *
    • {@link #testAuth()} - 设备认证
    • @@ -58,10 +66,16 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { private static final int SERVER_PORT = 8091; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + // TODO @AI:这里可以通过 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec 么?例如说:使用 vertx vertx tcp client???从而更好的复用解码逻辑; + /** + * 分隔符(需与 application.yaml 中的 delimiter 配置一致) + */ + private static final String DELIMITER = "\n"; -// private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); - private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + // ===================== 序列化器选择(Delimiter 模式推荐使用 JSON) ===================== + + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); +// private static final IotMessageSerializer SERIALIZER = new IotBinarySerializer(); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== @@ -82,18 +96,19 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); + // 1.2 序列化 + // TODO @AI:是不是把 SERIALIZER 放到 sendAndReceive 里; + byte[] payload = SERIALIZER.serialize(request); + log.info("[testAuth][Serializer: {}, 请求消息: {}, 数据包长度: {} 字节]", SERIALIZER.getType(), request, payload.length); // 2.1 发送请求 try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { socket.setSoTimeout(TIMEOUT_MS); byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 + // 2.2 反序列化响应 if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); + IotDeviceMessage response = SERIALIZER.deserialize(responseBytes); log.info("[testAuth][响应消息: {}]", response); } else { log.warn("[testAuth][未收到响应]"); @@ -119,17 +134,17 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { registerReqDTO.setProductSecret("test-product-secret"); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); + log.info("[testDeviceRegister][Serializer: {}, 请求消息: {}, 数据包长度: {} 字节]", SERIALIZER.getType(), request, payload.length); // 2.1 发送请求 try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { socket.setSoTimeout(TIMEOUT_MS); byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 + // 2.2 反序列化响应 if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); + IotDeviceMessage response = SERIALIZER.deserialize(responseBytes); log.info("[testDeviceRegister][响应消息: {}]", response); log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); } else { @@ -161,15 +176,15 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { .put("height", "2") .build()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); + log.info("[testPropertyPost][Serializer: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送请求 byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 + // 3.2 反序列化响应 if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); + IotDeviceMessage response = SERIALIZER.deserialize(responseBytes); log.info("[testPropertyPost][响应消息: {}]", response); } else { log.warn("[testPropertyPost][未收到响应]"); @@ -200,15 +215,15 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { MapUtil.builder().put("rice", 3).build(), System.currentTimeMillis()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); + log.info("[testEventPost][Serializer: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送请求 byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 + // 3.2 反序列化响应 if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); + IotDeviceMessage response = SERIALIZER.deserialize(responseBytes); log.info("[testEventPost][响应消息: {}]", response); } else { log.warn("[testEventPost][未收到响应]"); @@ -231,41 +246,44 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); + byte[] payload = SERIALIZER.serialize(request); byte[] responseBytes = sendAndReceive(socket, payload); if (responseBytes != null) { log.info("[authenticate][响应数据长度: {} 字节,首字节: 0x{}, HEX: {}]", responseBytes.length, String.format("%02X", responseBytes[0]), HexUtil.encodeHexStr(responseBytes)); - return CODEC.decode(responseBytes); + return SERIALIZER.deserialize(responseBytes); } return null; } /** - * 发送 TCP 请求并接收响应 + * 发送 TCP 请求并接收响应(支持 Delimiter 分隔符协议) + *

      + * 发送格式:[消息体][分隔符] + * 接收格式:[消息体][分隔符] * * @param socket TCP Socket - * @param payload 请求数据 - * @return 响应数据 + * @param payload 请求数据(消息体,不含分隔符) + * @return 响应数据(消息体,不含分隔符) */ private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - // 1. 发送请求 OutputStream out = socket.getOutputStream(); - InputStream in = socket.getInputStream(); - out.write(payload); - out.flush(); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); - // 2.1 等待一小段时间让服务器处理 - Thread.sleep(100); - // 2.2 接收响应 - byte[] buffer = new byte[4096]; + // 1. 发送请求(添加分隔符后缀) + out.write(payload); + out.write(DELIMITER.getBytes(StandardCharsets.UTF_8)); + out.flush(); + log.info("[sendAndReceive][发送数据: {} 字节(不含分隔符)]", payload.length); + + // 2. 接收响应(读取到分隔符为止) try { - int length = in.read(buffer); - if (length > 0) { - byte[] response = new byte[length]; - System.arraycopy(buffer, 0, response, 0, length); + String responseLine = in.readLine(); + if (responseLine != null) { + byte[] response = responseLine.getBytes(StandardCharsets.UTF_8); + log.info("[sendAndReceive][接收数据: {} 字节]", response.length); return response; } return null; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-binary-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-binary-packet-examples.md deleted file mode 100644 index d6b2b3fdb5..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-binary-packet-examples.md +++ /dev/null @@ -1,193 +0,0 @@ -# TCP 二进制协议数据包格式说明 - -## 1. 协议概述 - -TCP 二进制协议是一种高效的自定义协议格式,采用紧凑的二进制格式传输数据,适用于对带宽和性能要求较高的 IoT 场景。 - -### 1.1 协议特点 - -- **高效传输**:完全二进制格式,减少数据传输量 -- **版本控制**:内置协议版本号,支持协议升级 -- **类型安全**:明确的消息类型标识 -- **简洁设计**:去除冗余字段,协议更加精简 -- **兼容性**:与现有 `IotDeviceMessage` 接口完全兼容 - -## 2. 协议格式 - -### 2.1 整体结构 - -``` -+--------+--------+--------+---------------------------+--------+--------+ -| 魔术字 | 版本号 | 消息类型| 消息长度(4字节) | -+--------+--------+--------+---------------------------+--------+--------+ -| 消息 ID 长度(2字节) | 消息 ID (变长字符串) | -+--------+--------+--------+--------+--------+--------+--------+--------+ -| 方法名长度(2字节) | 方法名(变长字符串) | -+--------+--------+--------+--------+--------+--------+--------+--------+ -| 消息体数据(变长) | -+--------+--------+--------+--------+--------+--------+--------+--------+ -``` - -### 2.2 字段详细说明 - -| 字段 | 长度 | 类型 | 说明 | -|------|------|------|------| -| 魔术字 | 1字节 | byte | `0x7E` - 协议识别标识,用于数据同步 | -| 版本号 | 1字节 | byte | `0x01` - 协议版本号,支持版本控制 | -| 消息类型 | 1字节 | byte | `0x01`=请求, `0x02`=响应 | -| 消息长度 | 4字节 | int | 整个消息的总长度(包含头部) | -| 消息 ID 长度 | 2字节 | short | 消息 ID 字符串的字节长度 | -| 消息 ID | 变长 | string | 消息唯一标识符(UTF-8编码) | -| 方法名长度 | 2字节 | short | 方法名字符串的字节长度 | -| 方法名 | 变长 | string | 消息方法名(UTF-8编码) | -| 消息体 | 变长 | binary | 根据消息类型的不同数据格式 | - -**⚠️ 重要说明**:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 - -### 2.3 协议常量定义 - -```java -// 协议标识 -private static final byte MAGIC_NUMBER = (byte) 0x7E; -private static final byte PROTOCOL_VERSION = (byte) 0x01; - -// 消息类型 -private static final byte REQUEST = (byte) 0x01; // 请求消息 -private static final byte RESPONSE = (byte) 0x02; // 响应消息 - -// 协议长度 -private static final int HEADER_FIXED_LENGTH = 7; // 固定头部长度 -private static final int MIN_MESSAGE_LENGTH = 11; // 最小消息长度 -``` - -## 3. 消息类型和格式 - -### 3.1 请求消息 (REQUEST - 0x01) - -请求消息用于设备向服务器发送数据或请求。 - -#### 3.1.1 消息体格式 -``` -消息体 = params 数据(JSON格式) -``` - -#### 3.1.2 示例:设备认证请求 - -**消息内容:** -- 消息 ID: `auth_1704067200000_123` -- 方法名: `auth` -- 参数: `{"clientId":"device_001","username":"productKey_deviceName","password":"device_password"}` - -**二进制数据包结构:** -``` -7E // 魔术字 (0x7E) -01 // 版本号 (0x01) -01 // 消息类型 (REQUEST) -00 00 00 89 // 消息长度 (137字节) -00 19 // 消息 ID 长度 (25字节) -61 75 74 68 5F 31 37 30 34 30 // 消息 ID: "auth_1704067200000_123" -36 37 32 30 30 30 30 30 5F 31 -32 33 -00 04 // 方法名长度 (4字节) -61 75 74 68 // 方法名: "auth" -7B 22 63 6C 69 65 6E 74 49 64 // JSON参数数据 -22 3A 22 64 65 76 69 63 65 5F // {"clientId":"device_001", -30 30 31 22 2C 22 75 73 65 72 // "username":"productKey_deviceName", -6E 61 6D 65 22 3A 22 70 72 6F // "password":"device_password"} -64 75 63 74 4B 65 79 5F 64 65 -76 69 63 65 4E 61 6D 65 22 2C -22 70 61 73 73 77 6F 72 64 22 -3A 22 64 65 76 69 63 65 5F 70 -61 73 73 77 6F 72 64 22 7D -``` - -#### 3.1.3 示例:属性数据上报 - -**消息内容:** -- 消息 ID: `property_1704067200000_456` -- 方法名: `thing.property.post` -- 参数: `{"temperature":25.5,"humidity":60.2,"pressure":1013.25}` - -### 3.2 响应消息 (RESPONSE - 0x02) - -响应消息用于服务器向设备回复请求结果。 - -#### 3.2.1 消息体格式 -``` -消息体 = 响应码(4字节) + 响应消息长度(2字节) + 响应消息(UTF-8) + 响应数据(JSON) -``` - -#### 3.2.2 字段说明 - -| 字段 | 长度 | 类型 | 说明 | -|------|------|------|------| -| 响应码 | 4字节 | int | HTTP状态码风格,0=成功,其他=错误 | -| 响应消息长度 | 2字节 | short | 响应消息字符串的字节长度 | -| 响应消息 | 变长 | string | 响应提示信息(UTF-8编码) | -| 响应数据 | 变长 | binary | JSON格式的响应数据(可选) | - -#### 3.2.3 示例:认证成功响应 - -**消息内容:** -- 消息 ID: `auth_response_1704067200000_123` -- 方法名: `auth` -- 响应码: `0` -- 响应消息: `认证成功` -- 响应数据: `{"success":true,"message":"认证成功"}` - -**二进制数据包结构:** -``` -7E // 魔术字 (0x7E) -01 // 版本号 (0x01) -02 // 消息类型 (RESPONSE) -00 00 00 A4 // 消息长度 (164字节) -00 22 // 消息 ID 长度 (34字节) -61 75 74 68 5F 72 65 73 70 6F // 消息 ID: "auth_response_1704067200000_123" -6E 73 65 5F 31 37 30 34 30 36 -37 32 30 30 30 30 30 5F 31 32 -33 -00 04 // 方法名长度 (4字节) -61 75 74 68 // 方法名: "auth" -00 00 00 00 // 响应码 (0 = 成功) -00 0C // 响应消息长度 (12字节) -E8 AE A4 E8 AF 81 E6 88 90 E5 // 响应消息: "认证成功" (UTF-8) -8A 9F -7B 22 73 75 63 63 65 73 73 22 // JSON响应数据 -3A 74 72 75 65 2C 22 6D 65 73 // {"success":true,"message":"认证成功"} -73 61 67 65 22 3A 22 E8 AE A4 -E8 AF 81 E6 88 90 E5 8A 9F 22 -7D -``` - -## 4. 编解码器标识 - -```java -public static final String TYPE = "TCP_BINARY"; -``` - -## 5. 协议优势 - -- **数据紧凑**:二进制格式,相比 JSON 减少 30-50% 的数据量 -- **解析高效**:直接二进制操作,减少字符串转换开销 -- **类型安全**:明确的消息类型和字段定义 -- **设计简洁**:去除冗余字段,协议更加精简高效 -- **版本控制**:内置版本号支持协议升级 - -## 6. 与 JSON 协议对比 - -| 特性 | 二进制协议 | JSON协议 | -|------|-------------|--------| -| 数据大小 | 小(节省30-50%) | 大 | -| 解析性能 | 高 | 中等 | -| 网络开销 | 低 | 高 | -| 可读性 | 差 | 优秀 | -| 调试难度 | 高 | 低 | -| 扩展性 | 良好 | 优秀 | - -**推荐场景**: -- ✅ **高频数据传输**:传感器数据实时上报 -- ✅ **带宽受限环境**:移动网络、卫星通信 -- ✅ **性能要求高**:需要低延迟、高吞吐的场景 -- ✅ **设备资源有限**:嵌入式设备、低功耗设备 -- ❌ **开发调试阶段**:调试困难,建议使用 JSON 协议 -- ❌ **快速原型开发**:开发效率低 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-json-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-json-packet-examples.md deleted file mode 100644 index 09ef50cfe5..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/tcp-json-packet-examples.md +++ /dev/null @@ -1,191 +0,0 @@ -# TCP JSON 格式协议说明 - -## 1. 协议概述 - -TCP JSON 格式协议采用纯 JSON 格式进行数据传输,具有以下特点: - -- **标准化**:使用标准 JSON 格式,易于解析和处理 -- **可读性**:人类可读,便于调试和维护 -- **扩展性**:可以轻松添加新字段,向后兼容 -- **跨平台**:JSON 格式支持所有主流编程语言 -- **安全优化**:移除冗余的 deviceId 字段,提高安全性 - -## 2. 消息格式 - -### 2.1 基础消息结构 - -```json -{ - "id": "消息唯一标识", - "method": "消息方法", - "params": { - // 请求参数 - }, - "data": { - // 响应数据 - }, - "code": 响应码, - "msg": "响应消息", - "timestamp": 时间戳 -} -``` - -**⚠️ 重要说明**: -- **不包含 deviceId 字段**:由服务器通过 TCP 连接上下文自动确定设备 ID -- **避免伪造攻击**:防止设备伪造其他设备的 ID 发送消息 - -### 2.2 字段详细说明 - -| 字段名 | 类型 | 必填 | 用途 | 说明 | -|--------|------|------|------|------| -| id | String | 是 | 所有消息 | 消息唯一标识 | -| method | String | 是 | 所有消息 | 消息方法,如 `auth`、`thing.property.post` | -| params | Object | 否 | 请求消息 | 请求参数,具体内容根据method而定 | -| data | Object | 否 | 响应消息 | 响应数据,服务器返回的结果数据 | -| code | Integer | 否 | 响应消息 | 响应码,0=成功,其他=错误 | -| msg | String | 否 | 响应消息 | 响应提示信息 | -| timestamp | Long | 是 | 所有消息 | 时间戳(毫秒),编码时自动生成 | - -### 2.3 消息分类 - -#### 2.3.1 请求消息(上行) -- **特征**:包含 `params` 字段,不包含 `code`、`msg` 字段 -- **方向**:设备 → 服务器 -- **用途**:设备认证、数据上报、状态更新等 - -#### 2.3.2 响应消息(下行) -- **特征**:包含 `code`、`msg` 字段,可能包含 `data` 字段 -- **方向**:服务器 → 设备 -- **用途**:认证结果、指令响应、错误提示等 - -## 3. 消息示例 - -### 3.1 设备认证 (auth) - -#### 认证请求格式 -**消息方向**:设备 → 服务器 - -```json -{ - "id": "auth_1704067200000_123", - "method": "auth", - "params": { - "clientId": "device_001", - "username": "productKey_deviceName", - "password": "设备密码" - }, - "timestamp": 1704067200000 -} -``` - -**认证参数说明:** - -| 字段名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| clientId | String | 是 | 客户端唯一标识,用于连接管理 | -| username | String | 是 | 设备用户名,格式为 `productKey_deviceName` | -| password | String | 是 | 设备密码,在设备管理平台配置 | - -#### 认证响应格式 -**消息方向**:服务器 → 设备 - -**认证成功响应:** -```json -{ - "id": "response_auth_1704067200000_123", - "method": "auth", - "data": { - "success": true, - "message": "认证成功" - }, - "code": 0, - "msg": "认证成功", - "timestamp": 1704067200001 -} -``` - -**认证失败响应:** -```json -{ - "id": "response_auth_1704067200000_123", - "method": "auth", - "data": { - "success": false, - "message": "认证失败:用户名或密码错误" - }, - "code": 401, - "msg": "认证失败", - "timestamp": 1704067200001 -} -``` - -### 3.2 属性数据上报 (thing.property.post) - -**消息方向**:设备 → 服务器 - -**示例:温度传感器数据上报** -```json -{ - "id": "property_1704067200000_456", - "method": "thing.property.post", - "params": { - "temperature": 25.5, - "humidity": 60.2, - "pressure": 1013.25, - "battery": 85, - "signal_strength": -65 - }, - "timestamp": 1704067200000 -} -``` - -### 3.3 设备状态更新 (thing.state.update) - -**消息方向**:设备 → 服务器 - -**示例:心跳请求** -```json -{ - "id": "heartbeat_1704067200000_321", - "method": "thing.state.update", - "params": { - "state": "online", - "uptime": 86400, - "memory_usage": 65.2, - "cpu_usage": 12.8 - }, - "timestamp": 1704067200000 -} -``` - -## 4. 编解码器标识 - -```java -public static final String TYPE = "TCP_JSON"; -``` - -## 5. 协议优势 - -- **开发效率高**:JSON 格式,开发和调试简单 -- **跨语言支持**:所有主流语言都支持 JSON -- **可读性优秀**:可以直接查看消息内容 -- **扩展性强**:可以轻松添加新字段 -- **安全性高**:移除 deviceId 字段,防止伪造攻击 - -## 6. 与二进制协议对比 - -| 特性 | JSON协议 | 二进制协议 | -|------|----------|------------| -| 开发难度 | 低 | 高 | -| 调试难度 | 低 | 高 | -| 可读性 | 优秀 | 差 | -| 数据大小 | 中等 | 小(节省30-50%) | -| 解析性能 | 中等 | 高 | -| 学习成本 | 低 | 高 | - -**推荐场景**: -- ✅ **开发调试阶段**:调试友好,开发效率高 -- ✅ **快速原型开发**:实现简单,快速迭代 -- ✅ **多语言集成**:广泛的语言支持 -- ❌ **高频数据传输**:建议使用二进制协议 -- ❌ **带宽受限环境**:建议使用二进制协议 \ No newline at end of file From 1d8ab8ff3d88bcade12371af3c3f08499f24cd89 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 03:33:09 +0800 Subject: [PATCH 09/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20tcp=E3=80=81http=20=E5=8D=95=E6=B5=8B=E7=B1=BB?= =?UTF-8?q?=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...rectDeviceHttpProtocolIntegrationTest.java | 13 +- ...ewayDeviceHttpProtocolIntegrationTest.java | 34 +- ...ySubDeviceHttpProtocolIntegrationTest.java | 5 - ...irectDeviceTcpProtocolIntegrationTest.java | 284 +++++++------- ...tewayDeviceTcpProtocolIntegrationTest.java | 346 +++++++++--------- ...aySubDeviceTcpProtocolIntegrationTest.java | 235 ++++++------ 6 files changed, 436 insertions(+), 481 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java index 8dd36cc635..ea412a2079 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -92,9 +91,7 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post", SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) - .put("version", "1.0") .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("width", 1) .put("height", "2") @@ -126,9 +123,7 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post", SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) - .put("version", "1.0") .put("params", IotDeviceEventPostReqDTO.of( "eat", MapUtil.builder().put("rice", 3).build(), @@ -163,10 +158,10 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { // 1.1 构建请求 String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); // 1.2 构建请求参数 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO(); - reqDTO.setProductKey(PRODUCT_KEY); - reqDTO.setDeviceName("test-" + System.currentTimeMillis()); - reqDTO.setProductSecret("test-product-secret"); + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName("test-" + System.currentTimeMillis()) + .setProductSecret("test-product-secret"); String payload = JsonUtils.toJsonString(reqDTO); // 1.3 输出请求 log.info("[testDeviceRegister][请求 URL: {}]", url); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java index 354c4d6858..779c588b76 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -20,7 +19,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.Collections; -import java.util.List; import java.util.Map; @@ -121,9 +119,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); params.setSubDevices(Collections.singletonList(subDeviceAuth)); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod()) - .put("version", "1.0") .put("params", params) .build()); // 1.4 输出请求 @@ -155,9 +151,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { params.setSubDevices(Collections.singletonList( new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod()) - .put("version", "1.0") .put("params", params) .build()); // 1.3 输出请求 @@ -187,9 +181,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { // 1.2 构建请求参数(目前为空,预留扩展) IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod()) - .put("version", "1.0") .put("params", params) .build()); // 1.3 输出请求 @@ -208,8 +200,6 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { // ===================== 子设备注册测试 ===================== - // TODO @芋艿:待测试 - /** * 子设备动态注册测试 *

      @@ -227,9 +217,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); subDevice.setDeviceName("mougezishebei"); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod()) - .put("version", "1.0") .put("params", Collections.singletonList(subDevice)) .build()); // 1.3 输出请求 @@ -263,9 +251,9 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { .put("temperature", 25.5) .build(); // 1.3 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("message", "gateway started").build()) + .setTime(System.currentTimeMillis()); Map gatewayEvents = MapUtil.builder() .put("statusReport", gatewayEvent) .build(); @@ -274,26 +262,24 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { .put("power", 100) .build(); // 1.5 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("errorCode", 0).build()) + .setTime(System.currentTimeMillis()); Map subDeviceEvents = MapUtil.builder() .put("healthCheck", subDeviceEvent) .build(); // 1.6 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData() + .setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)) + .setProperties(subDeviceProperties) + .setEvents(subDeviceEvents); // 1.7 构建请求参数 IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); params.setProperties(gatewayProperties); params.setEvents(gatewayEvents); params.setSubDevices(ListUtil.of(subDeviceData)); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod()) - .put("version", "1.0") .put("params", params) .build()); // 1.8 输出请求 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java index cfebdbe3f8..f6b9399bcc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -94,9 +93,7 @@ public class IotGatewaySubDeviceHttpProtocolIntegrationTest { String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post", SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) - .put("version", "1.0") .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("power", 100) .put("status", "online") @@ -130,9 +127,7 @@ public class IotGatewaySubDeviceHttpProtocolIntegrationTest { String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post", SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) - .put("version", "1.0") .put("params", IotDeviceEventPostReqDTO.of( "alarm", MapUtil.builder() diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java index b386cd1455..29b751152b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java @@ -1,8 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.HexUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -10,40 +8,32 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; -import cn.iocoder.yudao.module.iot.gateway.serialize.binary.IotBinarySerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.Socket; -import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * IoT 直连设备 TCP 协议集成测试(手动测试) * *

      测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 TCP 协议直接连接平台 * - *

      支持两种序列化格式: - *

        - *
      • {@link IotJsonSerializer} - JSON 格式
      • - *
      • {@link IotBinarySerializer} - 二进制格式
      • - *
      - * - *

      TCP 拆包配置(需与 application.yaml 中的 codec 配置一致): - *

        - *
      • type: delimiter - 基于分隔符拆包
      • - *
      • delimiter: \n - 换行符作为分隔符
      • - *
      - * *

      使用步骤: *

        *
      1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
      2. - *
      3. 修改 {@link #SERIALIZER} 选择测试的序列化格式(Delimiter 模式只支持 JSON)
      4. *
      5. 运行以下测试方法: *
          *
        • {@link #testAuth()} - 设备认证
        • @@ -66,16 +56,25 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { private static final int SERVER_PORT = 8091; private static final int TIMEOUT_MS = 5000; - // TODO @AI:这里可以通过 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec 么?例如说:使用 vertx vertx tcp client???从而更好的复用解码逻辑; + private static Vertx vertx; + private static NetClient netClient; + + // ===================== 编解码器 ===================== + /** - * 分隔符(需与 application.yaml 中的 delimiter 配置一致) + * 消息序列化器 */ - private static final String DELIMITER = "\n"; - - // ===================== 序列化器选择(Delimiter 模式推荐使用 JSON) ===================== - private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); -// private static final IotMessageSerializer SERIALIZER = new IotBinarySerializer(); + + /** + * TCP 帧编解码器 + */ + private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodec.create( + new IotTcpConfig.CodecConfig() {{ + setType("delimiter"); + setDelimiter("\\n"); + }} + ); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== @@ -83,6 +82,25 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { private static final String DEVICE_NAME = "small"; private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + // ===================== 认证测试 ===================== /** @@ -90,29 +108,21 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 1.2 序列化 - // TODO @AI:是不是把 SERIALIZER 放到 sendAndReceive 里; - byte[] payload = SERIALIZER.serialize(request); - log.info("[testAuth][Serializer: {}, 请求消息: {}, 数据包长度: {} 字节]", SERIALIZER.getType(), request, payload.length); - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 反序列化响应 - if (responseBytes != null) { - IotDeviceMessage response = SERIALIZER.deserialize(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - } else { - log.warn("[testAuth][未收到响应]"); - } + // 2. 发送并接收响应 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testAuth][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -127,29 +137,22 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { */ @Test public void testDeviceRegister() throws Exception { - // 1.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-tcp-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 1.2 序列化 - byte[] payload = SERIALIZER.serialize(request); - log.info("[testDeviceRegister][Serializer: {}, 请求消息: {}, 数据包长度: {} 字节]", SERIALIZER.getType(), request, payload.length); + // 1. 构建注册消息 + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName("test-tcp-" + System.currentTimeMillis()) + .setProductSecret("test-product-secret"); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 反序列化响应 - if (responseBytes != null) { - IotDeviceMessage response = SERIALIZER.deserialize(responseBytes); - log.info("[testDeviceRegister][响应消息: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - } else { - log.warn("[testDeviceRegister][未收到响应]"); - } + // 2. 发送并接收响应 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testDeviceRegister][响应消息: {}]", response); + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); + } finally { + socket.close(); } } @@ -160,35 +163,25 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testPropertyPost][认证响应: {}]", authResponse); - // 2.1 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("width", 1) .put("height", "2") - .build()), - null, null, null); - // 2.2 序列化 - byte[] payload = SERIALIZER.serialize(request); - log.info("[testPropertyPost][Serializer: {}, 请求消息: {}]", SERIALIZER.getType(), request); + .build())); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 反序列化响应 - if (responseBytes != null) { - IotDeviceMessage response = SERIALIZER.deserialize(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testPropertyPost][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -199,98 +192,87 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testEventPost][认证响应: {}]", authResponse); - // 2.1 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), IotDeviceEventPostReqDTO.of( "eat", MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis()), - null, null, null); - // 2.2 序列化 - byte[] payload = SERIALIZER.serialize(request); - log.info("[testEventPost][Serializer: {}, 请求消息: {}]", SERIALIZER.getType(), request); + System.currentTimeMillis())); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 反序列化响应 - if (responseBytes != null) { - IotDeviceMessage response = SERIALIZER.deserialize(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testEventPost][响应消息: {}]", response); + } finally { + socket.close(); } } // ===================== 辅助方法 ===================== + /** + * 建立 TCP 连接 + * + * @return 连接 Future + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + /** * 执行设备认证 * * @param socket TCP 连接 * @return 认证响应消息 */ - private IotDeviceMessage authenticate(Socket socket) throws Exception { + private IotDeviceMessage authenticate(NetSocket socket) throws Exception { IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = SERIALIZER.serialize(request); - byte[] responseBytes = sendAndReceive(socket, payload); - if (responseBytes != null) { - log.info("[authenticate][响应数据长度: {} 字节,首字节: 0x{}, HEX: {}]", - responseBytes.length, - String.format("%02X", responseBytes[0]), - HexUtil.encodeHexStr(responseBytes)); - return SERIALIZER.deserialize(responseBytes); - } - return null; + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authInfo); + return sendAndReceive(socket, request); } /** - * 发送 TCP 请求并接收响应(支持 Delimiter 分隔符协议) - *

          - * 发送格式:[消息体][分隔符] - * 接收格式:[消息体][分隔符] + * 发送消息并接收响应 * - * @param socket TCP Socket - * @param payload 请求数据(消息体,不含分隔符) - * @return 响应数据(消息体,不含分隔符) + * @param socket TCP 连接 + * @param request 请求消息 + * @return 响应消息 */ - private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - OutputStream out = socket.getOutputStream(); - BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); - - // 1. 发送请求(添加分隔符后缀) - out.write(payload); - out.write(DELIMITER.getBytes(StandardCharsets.UTF_8)); - out.flush(); - log.info("[sendAndReceive][发送数据: {} 字节(不含分隔符)]", payload.length); - - // 2. 接收响应(读取到分隔符为止) - try { - String responseLine = in.readLine(); - if (responseLine != null) { - byte[] response = responseLine.getBytes(StandardCharsets.UTF_8); - log.info("[sendAndReceive][接收数据: {} 字节]", response.length); - return response; + private IotDeviceMessage sendAndReceive(NetSocket socket, IotDeviceMessage request) throws Exception { + // 1. 使用 FRAME_CODEC 创建解码器 + CompletableFuture responseFuture = new CompletableFuture<>(); + RecordParser parser = FRAME_CODEC.createDecodeParser(buffer -> { + try { + // 反序列化响应 + IotDeviceMessage response = SERIALIZER.deserialize(buffer.getBytes()); + responseFuture.complete(response); + } catch (Exception e) { + responseFuture.completeExceptionally(e); } - return null; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } + }); + socket.handler(parser); + + // 2.1 序列化 + 帧编码(复用 gateway 的编码逻辑) + byte[] serializedData = SERIALIZER.serialize(request); + Buffer frameData = FRAME_CODEC.encode(serializedData); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length()); + // 2.2 发送请求 + socket.write(frameData); + + // 3. 等待响应 + IotDeviceMessage response = responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", SERIALIZER.serialize(response).length); + return response; } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java index b417ceb9fa..171bf12fcb 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java @@ -13,35 +13,34 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; import java.util.Collections; -import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * IoT 网关设备 TCP 协议集成测试(手动测试) * *

          测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 TCP 协议管理子设备拓扑关系 * - *

          支持两种编解码格式: - *

            - *
          • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
          • - *
          • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
          • - *
          - * *

          使用步骤: *

            *
          1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
          2. - *
          3. 修改 {@link #CODEC} 选择测试的编解码格式
          4. *
          5. 运行以下测试方法: *
              *
            • {@link #testAuth()} - 网关设备认证
            • @@ -66,10 +65,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { private static final int SERVER_PORT = 8091; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + private static Vertx vertx; + private static NetClient netClient; - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + // ===================== 编解码器 ===================== + + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); + + /** + * TCP 帧编解码器 + */ + private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodec.create( + new IotTcpConfig.CodecConfig() {{ + setType("delimiter"); + setDelimiter("\\n"); + }} + ); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== @@ -83,6 +97,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { private static final String SUB_DEVICE_NAME = "chazuo-it"; private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + // ===================== 认证测试 ===================== /** @@ -90,29 +123,22 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - } else { - log.warn("[testAuth][未收到响应]"); - } + // 2. 发送并接收响应 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testAuth][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -123,9 +149,8 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testTopoAdd() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testTopoAdd][认证响应: {}]", authResponse); @@ -140,24 +165,16 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { // 2.2 构建请求参数 IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( + IotDeviceMessage request = IotDeviceMessage.requestOf( IdUtil.fastSimpleUUID(), IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - params, - null, null, null); - // 2.3 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); + params); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoAdd][响应消息: {}]", response); - } else { - log.warn("[testTopoAdd][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testTopoAdd][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -166,35 +183,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testTopoDelete() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testTopoDelete][认证响应: {}]", authResponse); - // 2.1 构建请求参数 + // 2. 构建请求参数 IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); params.setSubDevices(Collections.singletonList( new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), - params, - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); + params); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoDelete][响应消息: {}]", response); - } else { - log.warn("[testTopoDelete][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testTopoDelete][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -203,33 +210,23 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testTopoGet() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testTopoGet][认证响应: {}]", authResponse); - // 2.1 构建请求参数 + // 2. 构建请求参数 IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - params, - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); + params); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoGet][响应消息: {}]", response); - } else { - log.warn("[testTopoGet][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testTopoGet][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -240,35 +237,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testSubDeviceRegister() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testSubDeviceRegister][认证响应: {}]", authResponse); - // 2.1 构建请求参数 - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); - subDevice.setDeviceName("mougezishebei"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建请求参数 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO() + .setProductKey(SUB_DEVICE_PRODUCT_KEY) + .setDeviceName("mougezishebei"); + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - Collections.singletonList(subDevice), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); + Collections.singletonList(subDevice)); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testSubDeviceRegister][响应消息: {}]", response); - } else { - log.warn("[testSubDeviceRegister][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testSubDeviceRegister][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -279,9 +266,8 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testPropertyPackPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testPropertyPackPost][认证响应: {}]", authResponse); @@ -291,9 +277,9 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { .put("temperature", 25.5) .build(); // 2.2 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("message", "gateway started").build()) + .setTime(System.currentTimeMillis()); Map gatewayEvents = MapUtil.builder() .put("statusReport", gatewayEvent) .build(); @@ -302,97 +288,95 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { .put("power", 100) .build(); // 2.4 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("errorCode", 0).build()) + .setTime(System.currentTimeMillis()); Map subDeviceEvents = MapUtil.builder() .put("healthCheck", subDeviceEvent) .build(); // 2.5 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData() + .setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)) + .setProperties(subDeviceProperties) + .setEvents(subDeviceEvents); // 2.6 构建请求参数 IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); params.setProperties(gatewayProperties); params.setEvents(gatewayEvents); params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - params, - null, null, null); - // 2.7 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + params); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPackPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPackPost][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testPropertyPackPost][响应消息: {}]", response); + } finally { + socket.close(); } } // ===================== 辅助方法 ===================== + /** + * 建立 TCP 连接 + * + * @return 连接 Future + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + /** * 执行网关设备认证 * * @param socket TCP 连接 * @return 认证响应消息 */ - private IotDeviceMessage authenticate(Socket socket) throws Exception { + private IotDeviceMessage authenticate(NetSocket socket) throws Exception { IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); - byte[] responseBytes = sendAndReceive(socket, payload); - if (responseBytes != null) { - return CODEC.decode(responseBytes); - } - return null; + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authInfo); + return sendAndReceive(socket, request); } /** - * 发送 TCP 请求并接收响应 + * 发送消息并接收响应(复用 IotTcpFrameCodec 编解码逻辑) * - * @param socket TCP Socket - * @param payload 请求数据 - * @return 响应数据 + * @param socket TCP 连接 + * @param request 请求消息 + * @return 响应消息 */ - private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - // 1. 发送请求 - OutputStream out = socket.getOutputStream(); - InputStream in = socket.getInputStream(); - out.write(payload); - out.flush(); - - // 2.1 等待一小段时间让服务器处理 - Thread.sleep(100); - // 2.2 接收响应 - byte[] buffer = new byte[4096]; - try { - int length = in.read(buffer); - if (length > 0) { - byte[] response = new byte[length]; - System.arraycopy(buffer, 0, response, 0, length); - return response; + private IotDeviceMessage sendAndReceive(NetSocket socket, IotDeviceMessage request) throws Exception { + // 1. 使用 FRAME_CODEC 创建解码器(复用 gateway 的拆包逻辑) + CompletableFuture responseFuture = new CompletableFuture<>(); + RecordParser parser = FRAME_CODEC.createDecodeParser(buffer -> { + try { + // 反序列化响应 + IotDeviceMessage response = SERIALIZER.deserialize(buffer.getBytes()); + responseFuture.complete(response); + } catch (Exception e) { + responseFuture.completeExceptionally(e); } - return null; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } + }); + socket.handler(parser); + + // 2.1 序列化 + 帧编码(复用 gateway 的编码逻辑) + byte[] serializedData = SERIALIZER.serialize(request); + Buffer frameData = FRAME_CODEC.encode(serializedData); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length()); + // 2.2 发送请求 + socket.write(frameData); + + // 3. 等待响应 + IotDeviceMessage response = responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", + SERIALIZER.serialize(response).length); + return response; } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java index c918b474c3..4354313e1a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java @@ -1,23 +1,29 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * IoT 网关子设备 TCP 协议集成测试(手动测试) @@ -26,17 +32,10 @@ import java.net.Socket; * *

              重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 * - *

              支持两种编解码格式: - *

                - *
              • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
              • - *
              • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
              • - *
              - * *

              使用步骤: *

                *
              1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
              2. *
              3. 确保子设备已通过 {@link IotGatewayDeviceTcpProtocolIntegrationTest#testTopoAdd()} 绑定到网关
              4. - *
              5. 修改 {@link #CODEC} 选择测试的编解码格式
              6. *
              7. 运行以下测试方法: *
                  *
                • {@link #testAuth()} - 子设备认证
                • @@ -58,10 +57,25 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { private static final int SERVER_PORT = 8091; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + private static Vertx vertx; + private static NetClient netClient; - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + // ===================== 编解码器 ===================== + + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); + + /** + * TCP 帧编解码器 + */ + private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodec.create( + new IotTcpConfig.CodecConfig() {{ + setType("delimiter"); + setDelimiter("\\n"); + }} + ); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -69,6 +83,25 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { private static final String DEVICE_NAME = "chazuo-it"; private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + // ===================== 认证测试 ===================== /** @@ -76,28 +109,21 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - } else { - log.warn("[testAuth][未收到响应]"); - } + // 2. 发送并接收响应 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testAuth][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -108,37 +134,27 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testPropertyPost][认证响应: {}]", authResponse); - // 2.1 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("power", 100) .put("status", "online") .put("temperature", 36.5) - .build()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + .build())); log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testPropertyPost][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -149,16 +165,14 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testEventPost][认证响应: {}]", authResponse); - // 2.1 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), IotDeviceEventPostReqDTO.of( "alarm", @@ -168,78 +182,77 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { .put("threshold", 40) .put("current", 42) .build(), - System.currentTimeMillis()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + System.currentTimeMillis())); log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testEventPost][响应消息: {}]", response); + } finally { + socket.close(); } } // ===================== 辅助方法 ===================== + /** + * 建立 TCP 连接 + * + * @return 连接 Future + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + /** * 执行子设备认证 * * @param socket TCP 连接 * @return 认证响应消息 */ - private IotDeviceMessage authenticate(Socket socket) throws Exception { + private IotDeviceMessage authenticate(NetSocket socket) throws Exception { IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); - byte[] responseBytes = sendAndReceive(socket, payload); - if (responseBytes != null) { - return CODEC.decode(responseBytes); - } - return null; + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authInfo); + return sendAndReceive(socket, request); } /** - * 发送 TCP 请求并接收响应 + * 发送消息并接收响应(复用 IotTcpFrameCodec 编解码逻辑) * - * @param socket TCP Socket - * @param payload 请求数据 - * @return 响应数据 + * @param socket TCP 连接 + * @param request 请求消息 + * @return 响应消息 */ - private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - // 1. 发送请求 - OutputStream out = socket.getOutputStream(); - InputStream in = socket.getInputStream(); - out.write(payload); - out.flush(); - - // 2.1 等待一小段时间让服务器处理 - Thread.sleep(100); - // 2.2 接收响应 - byte[] buffer = new byte[4096]; - try { - int length = in.read(buffer); - if (length > 0) { - byte[] response = new byte[length]; - System.arraycopy(buffer, 0, response, 0, length); - return response; + private IotDeviceMessage sendAndReceive(NetSocket socket, IotDeviceMessage request) throws Exception { + // 1. 使用 FRAME_CODEC 创建解码器(复用 gateway 的拆包逻辑) + CompletableFuture responseFuture = new CompletableFuture<>(); + RecordParser parser = FRAME_CODEC.createDecodeParser(buffer -> { + try { + // 反序列化响应 + IotDeviceMessage response = SERIALIZER.deserialize(buffer.getBytes()); + responseFuture.complete(response); + } catch (Exception e) { + responseFuture.completeExceptionally(e); } - return null; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } + }); + socket.handler(parser); + + // 2.1 序列化 + 帧编码(复用 gateway 的编码逻辑) + byte[] serializedData = SERIALIZER.serialize(request); + Buffer frameData = FRAME_CODEC.encode(serializedData); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length()); + // 2.2 发送请求 + socket.write(frameData); + + // 3. 等待响应 + IotDeviceMessage response = responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", + SERIALIZER.serialize(response).length); + return response; } } From 046b2d51e96eb24138ce7b7dcb2f99e8b94c511f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 03:59:54 +0800 Subject: [PATCH 10/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91tcp=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=8870%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../protocol/http/IotHttpProtocol.java | 8 +- .../gateway/protocol/tcp/IotTcpProtocol.java | 25 +- .../upstream/IotTcpUpstreamHandler.java | 460 +++++------------- .../tcp/manager/IotTcpConnectionManager.java | 18 - 4 files changed, 144 insertions(+), 367 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java index b141afc5e4..51665e8f46 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java @@ -42,6 +42,7 @@ public class IotHttpProtocol implements IotProtocol { /** * 运行状态 */ + @Getter private volatile boolean running = false; /** @@ -61,6 +62,8 @@ public class IotHttpProtocol implements IotProtocol { public IotHttpProtocol(ProtocolInstanceProperties properties, IotMessageBus messageBus) { this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化下行消息订阅者 this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); } @@ -169,9 +172,4 @@ public class IotHttpProtocol implements IotProtocol { log.info("[stop][IoT HTTP 协议 {} 已停止]", getId()); } - @Override - public boolean isRunning() { - return running; - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java index c03383a224..9a59657a09 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; -import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; @@ -14,8 +13,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream.IotTcpU import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; import io.vertx.core.net.NetServer; import io.vertx.core.net.NetServerOptions; @@ -46,6 +43,7 @@ public class IotTcpProtocol implements IotProtocol { /** * 运行状态 */ + @Getter private volatile boolean running = false; /** @@ -72,6 +70,11 @@ public class IotTcpProtocol implements IotProtocol { */ private final IotTcpFrameCodec frameCodec; + /** + * TCP 连接管理器(每个 Protocol 实例独立) + */ + private final IotTcpConnectionManager connectionManager; + public IotTcpProtocol(ProtocolInstanceProperties properties, IotMessageBus messageBus, IotMessageSerializerManager serializerManager) { this.properties = properties; @@ -83,14 +86,15 @@ public class IotTcpProtocol implements IotProtocol { serializeType = IotSerializeTypeEnum.JSON; // 默认 JSON } this.serializer = serializerManager.get(serializeType); - // 初始化帧编解码器 IotTcpConfig tcpConfig = properties.getTcp(); IotTcpConfig.CodecConfig codecConfig = tcpConfig != null ? tcpConfig.getCodec() : null; this.frameCodec = IotTcpFrameCodec.create(codecConfig); + // 初始化连接管理器 + this.connectionManager = new IotTcpConnectionManager(); + // 初始化下行消息订阅者 - IotTcpConnectionManager connectionManager = SpringUtil.getBean(IotTcpConnectionManager.class); IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus); } @@ -131,12 +135,8 @@ public class IotTcpProtocol implements IotProtocol { // 1.3 创建服务器并设置连接处理器 tcpServer = vertx.createNetServer(options); - IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); - IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); - IotTcpConnectionManager connectionManager = SpringUtil.getBean(IotTcpConnectionManager.class); tcpServer.connectHandler(socket -> { - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, - connectionManager, frameCodec, serializer); + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(serverId, frameCodec, serializer, connectionManager); handler.handle(socket); }); @@ -197,9 +197,4 @@ public class IotTcpProtocol implements IotProtocol { log.info("[stop][IoT TCP 协议 {} 已停止]", getId()); } - @Override - public boolean isRunning() { - return running; - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java index c5b6267eba..2fb05cdd42 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java @@ -1,7 +1,5 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -15,7 +13,6 @@ import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; @@ -26,8 +23,10 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; -import java.util.Map; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; /** * TCP 上行消息处理器 @@ -39,14 +38,6 @@ public class IotTcpUpstreamHandler implements Handler { private static final String AUTH_METHOD = "auth"; - private final IotDeviceMessageService deviceMessageService; - - private final IotDeviceService deviceService; - - private final IotTcpConnectionManager connectionManager; - - private final IotDeviceCommonApi deviceApi; - private final String serverId; /** @@ -57,20 +48,30 @@ public class IotTcpUpstreamHandler implements Handler { * 消息序列化器(处理业务消息序列化/反序列化) */ private final IotMessageSerializer serializer; + /** + * TCP 连接管理器(每个 Protocol 实例独立) + */ + private final IotTcpConnectionManager connectionManager; - public IotTcpUpstreamHandler(IotProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotDeviceService deviceService, - IotTcpConnectionManager connectionManager, + // ===================== Spring 依赖(构造时注入) ===================== + + private final IotDeviceMessageService deviceMessageService; + private final IotDeviceService deviceService; + private final IotDeviceCommonApi deviceApi; + + public IotTcpUpstreamHandler(String serverId, IotTcpFrameCodec codec, - IotMessageSerializer serializer) { - this.serverId = protocol.getServerId(); + IotMessageSerializer serializer, + IotTcpConnectionManager connectionManager) { + Assert.notNull(codec, "TCP FrameCodec 必须配置"); + Assert.notNull(serializer, "消息序列化器必须配置"); + Assert.notNull(connectionManager, "连接管理器不能为空"); + this.serverId = serverId; this.codec = codec; this.serializer = serializer; this.connectionManager = connectionManager; - // TODO @AI:都通过 springutil 获取下; - this.deviceMessageService = deviceMessageService; - this.deviceService = deviceService; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.deviceService = SpringUtil.getBean(IotDeviceService.class); this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); } @@ -79,42 +80,30 @@ public class IotTcpUpstreamHandler implements Handler { String clientId = IdUtil.simpleUUID(); log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - // 设置异常和关闭处理器 + // 1. 设置异常和关闭处理器 socket.exceptionHandler(ex -> { log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - cleanupConnection(socket); + socket.close(); }); socket.closeHandler(v -> { log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); cleanupConnection(socket); }); - // 设置消息处理器(带拆包支持) + // 2.1 设置消息处理器 Handler messageHandler = buffer -> { - // TODO @AI:需要跟 AI 讨论。哪些情况关闭;哪些情况,发送异常消息; try { processMessage(clientId, buffer, socket); } catch (Exception e) { - // TODO @AI:这里能合并到 exceptionHandler 么?还是怎么搞好点; - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", + log.error("[handle][消息处理失败,客户端 ID: {},地址: {},错误: {}]", clientId, socket.remoteAddress(), e.getMessage()); - cleanupConnection(socket); socket.close(); } }; - - // 根据是否配置了 FrameCodec 来决定是否使用拆包器 - // TODO @AI:必须配置! - if (codec != null) { - // 使用拆包器处理粘包/拆包 - RecordParser parser = codec.createDecodeParser(messageHandler); - socket.handler(parser); - log.debug("[handle][启用 {} 拆包器,客户端 ID: {}]", codec.getType(), clientId); - } else { - // 未配置拆包器,直接处理原始数据(可能存在粘包问题) - socket.handler(messageHandler); - log.debug("[handle][未配置拆包器,客户端 ID: {}]", clientId); - } + // 2.2 使用拆包器处理粘包/拆包 + RecordParser parser = codec.createDecodeParser(messageHandler); + socket.handler(parser); + log.debug("[handle][启用 {} 拆包器,客户端 ID: {}]", codec.getType(), clientId); } /** @@ -123,49 +112,22 @@ public class IotTcpUpstreamHandler implements Handler { * @param clientId 客户端 ID * @param buffer 消息 * @param socket 网络连接 - * @throws Exception 消息解码失败时抛出异常 */ - private void processMessage(String clientId, Buffer buffer, NetSocket socket) throws Exception { - // 1. 基础检查 - // TODO @AI:不太应该为空?! - if (buffer == null || buffer.length() == 0) { - return; - } + private void processMessage(String clientId, Buffer buffer, NetSocket socket) { + // 1. 反序列化消息 + IotDeviceMessage message = serializer.deserialize(buffer.getBytes()); + Assert.notNull(message, "反序列化后消息为空"); - // 2. 反序列化消息 - IotDeviceMessage message; - try { - message = serializer.deserialize(buffer.getBytes()); - if (message == null) { - throw new IllegalArgumentException("反序列化后消息为空"); - } - } catch (Exception e) { - // TODO @AI:是不是不用 try catch? - throw new Exception("消息反序列化失败: " + e.getMessage(), e); - } - - // 3. 根据消息类型路由处理 - try { - if (AUTH_METHOD.equals(message.getMethod())) { - // 认证请求 - handleAuthenticationRequest(clientId, message, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(clientId, message, socket); - } else { - // 业务消息 - handleBusinessRequest(clientId, message, socket); - } - } catch (Exception e) { - // TODO @AI:如果参数不正确,不断开连接; - log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", clientId, message.getMethod(), e); - // 发送错误响应,避免客户端一直等待 - // TODO @AI:发送失败,是不是不用 try catch? - try { - sendErrorResponse(socket, message.getRequestId(), "消息处理失败"); - } catch (Exception responseEx) { - log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx); - } + // 2. 根据消息类型路由处理 + if (AUTH_METHOD.equals(message.getMethod())) { + // 认证请求 + handleAuthenticationRequest(clientId, message, socket); + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { + // 设备动态注册请求 + handleRegisterRequest(clientId, message, socket); + } else { + // 业务消息 + handleBusinessRequest(clientId, message, socket); } } @@ -177,51 +139,33 @@ public class IotTcpUpstreamHandler implements Handler { * @param socket 网络连接 */ private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, NetSocket socket) { - try { - // 1.1 解析认证参数 - // TODO @AI:直接 JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);然后,校验参数,不正确抛出 invalid exception;和 http 那一样; - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); - if (authParams == null) { - log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "认证参数不完整"); - return; - } - // 1.2 执行认证 - if (!validateDeviceAuth(authParams)) { - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", - clientId, authParams.getUsername()); - sendErrorResponse(socket, message.getRequestId(), "认证失败"); - return; - } + // 1. 解析认证参数 + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); + Assert.notNull(authParams, "认证参数不能为空"); + Assert.hasText(authParams.getUsername(), "username 不能为空"); + Assert.hasText(authParams.getPassword(), "password 不能为空"); - // 2.1 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); - // TODO @AI:这里就断言 deviceInfo 不为空了?! - if (deviceInfo == null) { - sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败"); - return; - } - // 2.2 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - // TODO @AI:这里就断言 device 不为空了?! - if (device == null) { - sendErrorResponse(socket, message.getRequestId(), "设备不存在"); - return; - } - - // 3.1 注册连接 - registerConnection(socket, device, clientId); - // 3.2 发送上线消息 - sendOnlineMessage(device); - // 3.3 发送成功响应 - sendSuccessResponse(socket, message.getRequestId(), "认证成功"); - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", - device.getId(), device.getDeviceName()); - } catch (Exception e) { - // TODO @AI:最大化去掉 try catch;(这个方法里的) - log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "认证处理异常"); + // 2.1 执行认证 + CommonResult authResult = deviceApi.authDevice(authParams); + if (authResult.isError()) { + log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", clientId, authParams.getUsername()); + sendErrorResponse(socket, message.getRequestId(), AUTH_METHOD, authResult.getCode(), authResult.getMsg()); + return; } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + + // 3.1 注册连接 + registerConnection(socket, device, clientId); + // 3.2 发送上线消息 + sendOnlineMessage(device); + // 3.3 发送成功响应 + sendSuccessResponse(socket, message.getRequestId(), AUTH_METHOD, "认证成功"); + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", device.getId(), device.getDeviceName()); } /** @@ -233,31 +177,25 @@ public class IotTcpUpstreamHandler implements Handler { * @see 阿里云 - 一型一密 */ private void handleRegisterRequest(String clientId, IotDeviceMessage message, NetSocket socket) { - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null) { - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "注册参数不完整"); - return; - } + // 1. 解析注册参数 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); + Assert.notNull(params, "注册参数不能为空"); + Assert.hasText(params.getProductKey(), "productKey 不能为空"); + Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); - // 2. 调用动态注册 - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendErrorResponse(socket, message.getRequestId(), result.getMsg()); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData()); - log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", - clientId, params.getDeviceName()); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "注册处理异常"); + // 2. 调用动态注册 + CommonResult result = deviceApi.registerDevice(params); + if (result.isError()) { + log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); + sendErrorResponse(socket, message.getRequestId(), IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), + result.getCode(), result.getMsg()); + return; } + + // 3. 发送成功响应 + sendSuccessResponse(socket, message.getRequestId(), + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); + log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", clientId, params.getDeviceName()); } /** @@ -268,25 +206,19 @@ public class IotTcpUpstreamHandler implements Handler { * @param socket 网络连接 */ private void handleBusinessRequest(String clientId, IotDeviceMessage message, NetSocket socket) { - try { - // 1. 检查认证状态 - if (connectionManager.isNotAuthenticated(socket)) { - log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "请先进行认证"); - return; - } - - // 2. 获取认证信息并处理业务消息 - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - - // 3. 发送消息到消息总线 - deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}", - clientId, message.toString()); - } catch (Exception e) { - log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e); + // 1. 获取认证信息并处理业务消息 + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo == null) { + log.error("[handleBusinessRequest][无法获取连接信息,客户端 ID: {}]", clientId); + sendErrorResponse(socket, message.getRequestId(), message.getMethod(), + UNAUTHORIZED.getCode(), "设备未认证,无法处理业务消息"); + return; } + + // 2. 发送消息到消息总线 + deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}", clientId, message); } /** @@ -302,7 +234,6 @@ public class IotTcpUpstreamHandler implements Handler { .setProductKey(device.getProductKey()) .setDeviceName(device.getDeviceName()) .setClientId(clientId); - // 注册连接 connectionManager.registerConnection(socket, device.getId(), connectionInfo); } @@ -312,13 +243,9 @@ public class IotTcpUpstreamHandler implements Handler { * @param device 设备信息 */ private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); - } + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); } /** @@ -327,70 +254,31 @@ public class IotTcpUpstreamHandler implements Handler { * @param socket 网络连接 */ private void cleanupConnection(NetSocket socket) { - try { - // 1. 发送离线消息(如果已认证) - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo != null) { - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - } - - // 2. 注销连接 - connectionManager.unregisterConnection(socket); - } catch (Exception e) { - log.error("[cleanupConnection][清理连接失败]", e); + // 1. 发送离线消息 + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo != null) { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); } + + // 2. 注销连接 + connectionManager.unregisterConnection(socket); } + // ===================== 发送响应消息 ===================== + /** - * 发送响应消息 + * 发送成功响应 * * @param socket 网络连接 - * @param success 是否成功 - * @param message 消息 * @param requestId 请求 ID + * @param method 方法名 + * @param data 响应数据 */ - private void sendResponse(NetSocket socket, boolean success, String message, String requestId) { - try { - // TODO @AI:是不是不用 - Object responseData = MapUtil.builder() - .put("success", success) - .put("message", message) - .build(); - - int code = success ? 0 : 401; - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, - code, message); - - // 序列化 + 帧编码 - byte[] serializedData = serializer.serialize(responseMessage); - Buffer frameData = codec.encode(serializedData); - socket.write(frameData); - - } catch (Exception e) { - log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); - } - } - - // TODO @AI:合并到 handleAuthenticationRequest 里; - /** - * 验证设备认证信息 - * - * @param authParams 认证参数 - * @return 是否认证成功 - */ - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) - .setPassword(authParams.getPassword())); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); - return false; - } + private void sendSuccessResponse(NetSocket socket, String requestId, String method, Object data) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null); + writeResponse(socket, responseMessage); } /** @@ -398,111 +286,25 @@ public class IotTcpUpstreamHandler implements Handler { * * @param socket 网络连接 * @param requestId 请求 ID - * @param errorMessage 错误消息 + * @param method 方法名 + * @param code 错误码 + * @param msg 错误消息 */ - private void sendErrorResponse(NetSocket socket, String requestId, String errorMessage) { - sendResponse(socket, false, errorMessage, requestId); + private void sendErrorResponse(NetSocket socket, String requestId, String method, Integer code, String msg) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg); + writeResponse(socket, responseMessage); } /** - * 发送成功响应 + * 写入响应到 Socket * - * @param socket 网络连接 - * @param requestId 请求 ID - * @param message 消息 + * @param socket 网络连接 + * @param responseMessage 响应消息 */ - @SuppressWarnings("SameParameterValue") - private void sendSuccessResponse(NetSocket socket, String requestId, String message) { - sendResponse(socket, true, message, requestId); - } - - /** - * 解析认证参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 认证参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceAuthReqDTO parseAuthParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceAuthReqDTO() - .setClientId(MapUtil.getStr(paramMap, "clientId")) - .setUsername(MapUtil.getStr(paramMap, "username")) - .setPassword(MapUtil.getStr(paramMap, "password")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceAuthReqDTO) { - return (IotDeviceAuthReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); - } catch (Exception e) { - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); - return null; - } - } - - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceRegisterReqDTO() - .setProductKey(MapUtil.getStr(paramMap, "productKey")) - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceRegisterReqDTO) { - return (IotDeviceRegisterReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - String jsonStr = JsonUtils.toJsonString(params); - return JsonUtils.parseObject(jsonStr, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param socket 网络连接 - * @param requestId 请求 ID - * @param registerResp 注册响应 - */ - private void sendRegisterSuccessResponse(NetSocket socket, String requestId, - IotDeviceRegisterRespDTO registerResp) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - // 2. 序列化 + 帧编码 - byte[] serializedData = serializer.serialize(responseMessage); - Buffer frameData = codec.encode(serializedData); - socket.write(frameData); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e); - } + private void writeResponse(NetSocket socket, IotDeviceMessage responseMessage) { + byte[] serializedData = serializer.serialize(responseMessage); + Buffer frameData = codec.encode(serializedData); + socket.write(frameData); } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java index e236a6db9f..2197f98f6e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java @@ -4,7 +4,6 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -20,7 +19,6 @@ import java.util.concurrent.ConcurrentHashMap; * @author 芋道源码 */ @Slf4j -@Component public class IotTcpConnectionManager { /** @@ -73,22 +71,6 @@ public class IotTcpConnectionManager { log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress()); } - /** - * 检查连接是否已认证 - */ - public boolean isAuthenticated(NetSocket socket) { - ConnectionInfo info = connectionMap.get(socket); - return info != null; - } - - // TODO @AI:是不是可以去掉;因为现在只有认证成功的,才会注册连接; - /** - * 检查连接是否未认证 - */ - public boolean isNotAuthenticated(NetSocket socket) { - return !isAuthenticated(socket); - } - /** * 获取连接信息 */ From 5ead81beb1d6560815f86b680486fa9266ce95cb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 04:25:14 +0800 Subject: [PATCH 11/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91tcp=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=8890%=EF=BC=89?= =?UTF-8?q?=EF=BC=9A=E4=BC=98=E5=8C=96=20codec=20=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gateway/protocol/tcp/IotTcpConfig.java | 13 +-- .../gateway/protocol/tcp/IotTcpProtocol.java | 3 +- .../tcp/codec/IotTcpCodecTypeEnum.java | 33 +----- .../protocol/tcp/codec/IotTcpFrameCodec.java | 21 ---- .../tcp/codec/IotTcpFrameCodecFactory.java | 27 +++++ .../delimiter/IotTcpDelimiterFrameCodec.java | 37 +------ .../length/IotTcpFixedLengthFrameCodec.java | 13 ++- .../length/IotTcpLengthFieldFrameCodec.java | 103 ++++++++++-------- .../downstream/IotTcpDownstreamHandler.java | 15 +-- .../src/main/resources/application.yaml | 8 +- ...irectDeviceTcpProtocolIntegrationTest.java | 18 ++- ...tewayDeviceTcpProtocolIntegrationTest.java | 18 ++- ...aySubDeviceTcpProtocolIntegrationTest.java | 18 ++- 13 files changed, 157 insertions(+), 170 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java index c967ce2764..16dd3b50e5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java @@ -64,13 +64,13 @@ public class IotTcpConfig { /** * LENGTH_FIELD: 长度字段偏移量 *

                  - * 表示长度字段在消息中的起始位置(从0开始) + * 表示长度字段在消息中的起始位置(从 0 开始) */ private Integer lengthFieldOffset; /** * LENGTH_FIELD: 长度字段长度(字节数) *

                  - * 常见值:1(最大255)、2(最大65535)、4(最大2GB) + * 常见值:1(最大 255)、2(最大 65535)、4(最大 2GB) */ private Integer lengthFieldLength; /** @@ -100,15 +100,6 @@ public class IotTcpConfig { */ private Integer fixedLength; - /** - * 最大帧长度(字节) - *

                  - * 防止内存溢出,默认 1MB - */ - @NotNull(message = "最大帧长度不能为空") - @Min(value = 1, message = "最大帧长度必须大于 0") - private Integer maxFrameLength = 1048576; - } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java index 9a59657a09..867b7dfa93 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream.IotTcpUpstreamHandler; @@ -89,7 +90,7 @@ public class IotTcpProtocol implements IotProtocol { // 初始化帧编解码器 IotTcpConfig tcpConfig = properties.getTcp(); IotTcpConfig.CodecConfig codecConfig = tcpConfig != null ? tcpConfig.getCodec() : null; - this.frameCodec = IotTcpFrameCodec.create(codecConfig); + this.frameCodec = IotTcpFrameCodecFactory.create(codecConfig); // 初始化连接管理器 this.connectionManager = new IotTcpConnectionManager(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java index 7b4b669112..344001f56f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java @@ -1,15 +1,12 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; import cn.hutool.core.util.ArrayUtil; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter.IotTcpDelimiterFrameCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpFixedLengthFrameCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpLengthFieldFrameCodec; import lombok.AllArgsConstructor; import lombok.Getter; -import java.util.function.Function; - /** * IoT TCP 拆包类型枚举 * @@ -21,38 +18,28 @@ public enum IotTcpCodecTypeEnum { /** * 基于固定长度的拆包 - *

                  - * 消息格式:固定长度的消息体 - * 需要配置:fixedLength(固定长度) */ - FIXED_LENGTH("fixed_length", IotTcpFixedLengthFrameCodec::new), + FIXED_LENGTH("fixed_length", IotTcpFixedLengthFrameCodec.class), /** * 基于分隔符的拆包 - *

                  - * 消息格式:消息内容 + 分隔符 - * 需要配置:delimiter(分隔符) */ - DELIMITER("delimiter", IotTcpDelimiterFrameCodec::new), + DELIMITER("delimiter", IotTcpDelimiterFrameCodec.class), /** * 基于长度字段的拆包 - *

                  - * 消息格式:[长度字段][消息体] - * 需要配置:lengthFieldOffset(长度字段偏移量)、lengthFieldLength(长度字段长度) */ - LENGTH_FIELD("length_field", IotTcpLengthFieldFrameCodec::new), + LENGTH_FIELD("length_field", IotTcpLengthFieldFrameCodec.class), ; /** * 类型标识 */ private final String type; - /** - * Codec 创建工厂 + * 编解码器类 */ - private final Function codecFactory; + private final Class codecClass; /** * 根据类型获取枚举 @@ -64,14 +51,4 @@ public enum IotTcpCodecTypeEnum { return ArrayUtil.firstMatch(e -> e.getType().equalsIgnoreCase(type), values()); } - /** - * 创建 Codec 实例 - * - * @param config 拆包配置 - * @return Codec 实例 - */ - public IotTcpFrameCodec createCodec(IotTcpConfig.CodecConfig config) { - return codecFactory.apply(config); - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java index 7ee16d3a0d..d002d2043f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; @@ -41,24 +40,4 @@ public interface IotTcpFrameCodec { */ Buffer encode(byte[] data); - // TODO @AI:还是搞个 facory 类 ,更好理解; - // ========== 静态工厂方法 ========== - - /** - * 根据配置创建编解码器 - * - * @param config 拆包配置 - * @return 编解码器实例,如果配置为空则返回 null - */ - static IotTcpFrameCodec create(IotTcpConfig.CodecConfig config) { - if (config == null) { - return null; - } - IotTcpCodecTypeEnum type = IotTcpCodecTypeEnum.of(config.getType()); - if (type == null) { - return null; - } - return type.createCodec(config); - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java new file mode 100644 index 0000000000..a40783ac6f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; + +/** + * IoT TCP 帧编解码器工厂 + * + * @author 芋道源码 + */ +public class IotTcpFrameCodecFactory { + + /** + * 根据配置创建编解码器 + * + * @param config 拆包配置 + * @return 编解码器实例,如果配置为空则返回 null + */ + public static IotTcpFrameCodec create(IotTcpConfig.CodecConfig config) { + Assert.notNull(config, "CodecConfig 不能为空"); + IotTcpCodecTypeEnum type = IotTcpCodecTypeEnum.of(config.getType()); + Assert.notNull(type, "不支持的 CodecType 类型:" + config.getType()); + return ReflectUtil.newInstance(type.getCodecClass(), config); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java index 6d4b8b009a..76e6c4b250 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; @@ -8,6 +7,7 @@ import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; /** * IoT TCP 分隔符帧编解码器 @@ -27,24 +27,14 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { - private final IotTcpConfig.CodecConfig config; - /** * 解析后的分隔符字节数组 */ private final byte[] delimiterBytes; - /** - * 最大帧长度 - */ - // TODO @AI:最大帧数要不去掉;简洁一点;包括其他地方的配置项; - private final int maxFrameLength; - public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) { - this.config = config; - // TODO @AI:禁止为空; + Assert.hasText(config.getDelimiter(), "delimiter 不能为空"); this.delimiterBytes = parseDelimiter(config.getDelimiter()); - this.maxFrameLength = config.getMaxFrameLength() != null ? config.getMaxFrameLength() : 1048576; } @Override @@ -55,20 +45,11 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { @Override public RecordParser createDecodeParser(Handler handler) { RecordParser parser = RecordParser.newDelimited(Buffer.buffer(delimiterBytes)); - - parser.handler(buffer -> { - // 检查帧长度是否超过限制 - if (buffer.length() > maxFrameLength) { - log.warn("[createDecodeParser][帧长度超过限制,length: {}, maxFrameLength: {}]", - buffer.length(), maxFrameLength); - return; - } - // 处理完整消息(不包含分隔符) - handler.handle(buffer); + // 处理完整消息(不包含分隔符) + parser.handler(handler); + parser.exceptionHandler(ex -> { + throw new RuntimeException("[createDecodeParser][解析异常]", ex); }); - - // TODO @AI:异常处理; - parser.exceptionHandler(ex -> log.error("[createDecodeParser][解析异常]", ex)); return parser; } @@ -89,13 +70,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { * @return 分隔符字节数组 */ private byte[] parseDelimiter(String delimiter) { - if (StrUtil.isBlank(delimiter)) { - // 默认使用换行符 - return new byte[]{'\n'}; - } - // 处理转义字符 - // TODO @AI:是否必要?不调整感觉也没问题?用户自己写对就 ok 了是哇? String parsed = delimiter .replace("\\r\\n", "\r\n") .replace("\\r", "\r") diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java index c9269fc383..092b485be8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java @@ -7,6 +7,7 @@ import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; /** * IoT TCP 定长帧编解码器 @@ -24,8 +25,8 @@ public class IotTcpFixedLengthFrameCodec implements IotTcpFrameCodec { private final int fixedLength; public IotTcpFixedLengthFrameCodec(IotTcpConfig.CodecConfig config) { - // TODO @AI:config.getFixedLength() 禁止为空; - this.fixedLength = config.getFixedLength() != null ? config.getFixedLength() : 1024; + Assert.notNull(config.getFixedLength(), "fixedLength 不能为空"); + this.fixedLength = config.getFixedLength(); } @Override @@ -37,8 +38,9 @@ public class IotTcpFixedLengthFrameCodec implements IotTcpFrameCodec { public RecordParser createDecodeParser(Handler handler) { RecordParser parser = RecordParser.newFixed(fixedLength); parser.handler(handler); - // TODO @AI:解析失败,是不是要抛出异常?因为要 close 掉连接; - parser.exceptionHandler(ex -> log.error("[createDecodeParser][解析异常]", ex)); + parser.exceptionHandler(ex -> { + throw new RuntimeException("[createDecodeParser][解析异常]", ex); + }); return parser; } @@ -46,8 +48,7 @@ public class IotTcpFixedLengthFrameCodec implements IotTcpFrameCodec { public Buffer encode(byte[] data) { Buffer buffer = Buffer.buffer(fixedLength); buffer.appendBytes(data); - // 如果数据不足固定长度,填充 0 - // TODO @AI:这里的填充是合理的么?RecordParser.newFixed(fixedLength) 有填充的逻辑么? + // 如果数据不足固定长度,填充 0(RecordParser.newFixed 解码时按固定长度读取,所以发送端需要填充) if (data.length < fixedLength) { byte[] padding = new byte[fixedLength - data.length]; buffer.appendBytes(padding); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java index 12a0b680eb..0fbe42d7c9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java @@ -7,6 +7,9 @@ import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; + +import java.util.concurrent.atomic.AtomicReference; /** * IoT TCP 长度字段帧编解码器 @@ -30,8 +33,6 @@ public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec { private final int lengthFieldLength; private final int lengthAdjustment; private final int initialBytesToStrip; - // TODO @AI:去掉 maxFrameLength 相关字段; - private final int maxFrameLength; /** * 头部长度 = 长度字段偏移量 + 长度字段长度 @@ -39,12 +40,14 @@ public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec { private final int headerLength; public IotTcpLengthFieldFrameCodec(IotTcpConfig.CodecConfig config) { - // TODO @AI: 增加参数校验;不要 default 逻辑; - this.lengthFieldOffset = config.getLengthFieldOffset() != null ? config.getLengthFieldOffset() : 0; - this.lengthFieldLength = config.getLengthFieldLength() != null ? config.getLengthFieldLength() : 4; - this.lengthAdjustment = config.getLengthAdjustment() != null ? config.getLengthAdjustment() : 0; - this.initialBytesToStrip = config.getInitialBytesToStrip() != null ? config.getInitialBytesToStrip() : 0; - this.maxFrameLength = config.getMaxFrameLength() != null ? config.getMaxFrameLength() : 1048576; + Assert.notNull(config.getLengthFieldOffset(), "lengthFieldOffset 不能为空"); + Assert.notNull(config.getLengthFieldLength(), "lengthFieldLength 不能为空"); + Assert.notNull(config.getLengthAdjustment(), "lengthAdjustment 不能为空"); + Assert.notNull(config.getInitialBytesToStrip(), "initialBytesToStrip 不能为空"); + this.lengthFieldOffset = config.getLengthFieldOffset(); + this.lengthFieldLength = config.getLengthFieldLength(); + this.lengthAdjustment = config.getLengthAdjustment(); + this.initialBytesToStrip = config.getInitialBytesToStrip(); this.headerLength = lengthFieldOffset + lengthFieldLength; } @@ -57,49 +60,45 @@ public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec { public RecordParser createDecodeParser(Handler handler) { // 创建状态机:先读取头部,再读取消息体 RecordParser parser = RecordParser.newFixed(headerLength); - // 使用数组保存状态和头部数据 - // TODO @AI:bodyLength 只使用第 0 位,是不是 atomicInteger 更合适? - final int[] bodyLength = {-1}; - final Buffer[] headerBuffer = {null}; + final AtomicReference bodyLength = new AtomicReference<>(null); // 消息体长度,null 表示读取头部阶段 + final AtomicReference headerBuffer = new AtomicReference<>(null); // 头部消息 // 处理读取到的数据 parser.handler(buffer -> { - if (bodyLength[0] == -1) { + if (bodyLength.get() == null) { // 阶段 1: 读取头部,解析长度字段 - headerBuffer[0] = buffer.copy(); + headerBuffer.set(buffer.copy()); int length = readLength(buffer, lengthFieldOffset, lengthFieldLength); int frameBodyLength = length + lengthAdjustment; - // 检查帧长度是否超过限制 - if (frameBodyLength < 0 || frameBodyLength > maxFrameLength - headerLength) { - log.warn("[createDecodeParser][帧长度异常,length: {}, frameBodyLength: {}, maxFrameLength: {}]", - length, frameBodyLength, maxFrameLength); - return; + // 检查帧长度是否合法 + if (frameBodyLength < 0) { + throw new IllegalStateException(String.format( + "[createDecodeParser][帧长度异常,length: %d, frameBodyLength: %d]", + length, frameBodyLength)); + } + // 消息体为空,抛出异常 + if (frameBodyLength == 0) { + throw new IllegalStateException("[createDecodeParser][消息体不能为空]"); } - if (frameBodyLength == 0) { - // 消息体为空,直接处理 - // TODO @AI:消息体为空,是不是不合理哈?应该抛出异常? - Buffer frame = processFrame(headerBuffer[0], null); - handler.handle(frame); - } else { - // 切换到读取消息体模式 - bodyLength[0] = frameBodyLength; - parser.fixedSizeMode(frameBodyLength); - } + // 【重要】切换到读取消息体模式 + bodyLength.set(frameBodyLength); + parser.fixedSizeMode(frameBodyLength); } else { // 阶段 2: 读取消息体,组装完整帧 - Buffer frame = processFrame(headerBuffer[0], buffer); + Buffer frame = processFrame(headerBuffer.get(), buffer); // 重置状态,准备读取下一帧 - bodyLength[0] = -1; - headerBuffer[0] = null; + bodyLength.set(null); + headerBuffer.set(null); parser.fixedSizeMode(headerLength); - // 处理完整消息 + // 【重要】处理完整消息 handler.handle(frame); } }); - - parser.exceptionHandler(ex -> log.error("[createDecodeParser][解析异常]", ex)); + parser.exceptionHandler(ex -> { + throw new RuntimeException("[createDecodeParser][解析异常]", ex); + }); return parser; } @@ -122,26 +121,36 @@ public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec { /** * 从 Buffer 中读取长度字段 */ - // TODO @AI:兼容 JDK8 + @SuppressWarnings("EnhancedSwitchMigration") private int readLength(Buffer buffer, int offset, int length) { - return switch (length) { - case 1 -> buffer.getUnsignedByte(offset); - case 2 -> buffer.getUnsignedShort(offset); - case 4 -> buffer.getInt(offset); - default -> throw new IllegalArgumentException("不支持的长度字段长度: " + length); - }; + switch (length) { + case 1: + return buffer.getUnsignedByte(offset); + case 2: + return buffer.getUnsignedShort(offset); + case 4: + return buffer.getInt(offset); + default: + throw new IllegalArgumentException("不支持的长度字段长度: " + length); + } } /** * 向 Buffer 中写入长度字段 */ - // TODO @AI:兼容 JDK8 private void writeLength(Buffer buffer, int length, int fieldLength) { switch (fieldLength) { - case 1 -> buffer.appendByte((byte) length); - case 2 -> buffer.appendShort((short) length); - case 4 -> buffer.appendInt(length); - default -> throw new IllegalArgumentException("不支持的长度字段长度: " + fieldLength); + case 1: + buffer.appendByte((byte) length); + break; + case 2: + buffer.appendShort((short) length); + break; + case 4: + buffer.appendInt(length); + break; + default: + throw new IllegalArgumentException("不支持的长度字段长度: " + fieldLength); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java index c87eebb39c..986bfbe60d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java @@ -39,8 +39,8 @@ public class IotTcpDownstreamHandler { IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( message.getDeviceId()); if (connectionInfo == null) { - // TODO @AI:是不是把消息 id 也打印进去?类似上面的日志 - log.warn("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId()); + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); return; } @@ -50,14 +50,11 @@ public class IotTcpDownstreamHandler { // 3. 发送到设备 boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes()); - // TODO @AI:不成功,直接抛出异常;反正下面的日志也会打印失败的 - if (success) { - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", - message.getDeviceId(), message.getMethod(), message.getId(), frameData.length()); - } else { - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); + if (!success) { + throw new RuntimeException("下行消息发送失败"); } + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), frameData.length()); } catch (Exception e) { log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", message.getDeviceId(), message.getMethod(), message, e); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 16ae6298d3..c5c8d52b09 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -68,7 +68,13 @@ yudao: codec: type: delimiter # 拆包类型:length_field / delimiter / fixed_length delimiter: "\\n" # 分隔符(支持转义:\\n=换行, \\r=回车, \\t=制表符) - max-frame-length: 1048576 # 最大帧长度(字节) +# type: length_field # 拆包类型:length_field / delimiter / fixed_length +# length-field-offset: 0 # 长度字段偏移量 +# length-field-length: 4 # 长度字段长度 +# length-adjustment: 0 # 长度调整值 +# initial-bytes-to-strip: 4 # 初始跳过的字节数 +# type: fixed_length # 拆包类型:length_field / delimiter / fixed_length +# fixed-length: 256 # 固定长度 # 协议配置(旧版,保持兼容) protocol: diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java index 29b751152b..4f3fe2daf2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java @@ -8,7 +8,9 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.vertx.core.Vertx; @@ -69,11 +71,17 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { /** * TCP 帧编解码器 */ - private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodec.create( - new IotTcpConfig.CodecConfig() {{ - setType("delimiter"); - setDelimiter("\\n"); - }} + private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodecFactory.create( + new IotTcpConfig.CodecConfig() + .setType(IotTcpCodecTypeEnum.DELIMITER.getType()) + .setDelimiter("\\n") +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setLengthFieldOffset(0) +// .setLengthFieldLength(4) +// .setLengthAdjustment(0) +// .setInitialBytesToStrip(4) +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setFixedLength(256) ); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java index 171bf12fcb..2efbd4d677 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java @@ -13,7 +13,9 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.vertx.core.Vertx; @@ -78,11 +80,17 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { /** * TCP 帧编解码器 */ - private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodec.create( - new IotTcpConfig.CodecConfig() {{ - setType("delimiter"); - setDelimiter("\\n"); - }} + private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodecFactory.create( + new IotTcpConfig.CodecConfig() + .setType(IotTcpCodecTypeEnum.DELIMITER.getType()) + .setDelimiter("\\n") +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setLengthFieldOffset(0) +// .setLengthFieldLength(4) +// .setLengthAdjustment(0) +// .setInitialBytesToStrip(4) +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setFixedLength(256) ); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java index 4354313e1a..1980d0a08c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java @@ -7,7 +7,9 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.vertx.core.Vertx; @@ -70,11 +72,17 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { /** * TCP 帧编解码器 */ - private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodec.create( - new IotTcpConfig.CodecConfig() {{ - setType("delimiter"); - setDelimiter("\\n"); - }} + private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodecFactory.create( + new IotTcpConfig.CodecConfig() + .setType(IotTcpCodecTypeEnum.DELIMITER.getType()) + .setDelimiter("\\n") +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setLengthFieldOffset(0) +// .setLengthFieldLength(4) +// .setLengthAdjustment(0) +// .setInitialBytesToStrip(4) +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setFixedLength(256) ); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== From 09041a24d765ce8649d9607e56fbccdfb5491429 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 04:51:18 +0800 Subject: [PATCH 12/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91tcp=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=88100%=EF=BC=89?= =?UTF-8?q?=EF=BC=9A=E5=9F=BA=E4=BA=8E=20code=20review=20=E8=BF=9B?= =?UTF-8?q?=E4=B8=80=E6=AD=A5=E5=AE=8C=E5=96=84=EF=BC=8C=E5=AF=B9=E5=BA=94?= =?UTF-8?q?=20iot-tcp-fix-plan.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gateway/protocol/tcp/IotTcpProtocol.java | 15 +++--- .../delimiter/IotTcpDelimiterFrameCodec.java | 9 +++- .../length/IotTcpFixedLengthFrameCodec.java | 5 ++ .../upstream/IotTcpUpstreamHandler.java | 52 +++++++++++++------ .../tcp/manager/IotTcpConnectionManager.java | 30 ++++++++++- 5 files changed, 87 insertions(+), 24 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java index 867b7dfa93..d1f894e1a9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java @@ -20,6 +20,7 @@ import io.vertx.core.net.NetServerOptions; import io.vertx.core.net.PemKeyCertOptions; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; /** * IoT TCP 协议实现 @@ -89,11 +90,12 @@ public class IotTcpProtocol implements IotProtocol { this.serializer = serializerManager.get(serializeType); // 初始化帧编解码器 IotTcpConfig tcpConfig = properties.getTcp(); - IotTcpConfig.CodecConfig codecConfig = tcpConfig != null ? tcpConfig.getCodec() : null; - this.frameCodec = IotTcpFrameCodecFactory.create(codecConfig); + Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); + Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); + this.frameCodec = IotTcpFrameCodecFactory.create(tcpConfig.getCodec()); // 初始化连接管理器 - this.connectionManager = new IotTcpConnectionManager(); + this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections()); // 初始化下行消息订阅者 IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); @@ -117,7 +119,7 @@ public class IotTcpProtocol implements IotProtocol { return; } - // 1.1 创建 Vertx 实例(每个 Protocol 独立管理) + // 1.1 创建 Vertx 实例 this.vertx = Vertx.vertx(); // 1.2 创建服务器选项 @@ -126,8 +128,9 @@ public class IotTcpProtocol implements IotProtocol { .setPort(properties.getPort()) .setTcpKeepAlive(true) .setTcpNoDelay(true) - .setReuseAddress(true); - if (tcpConfig != null && Boolean.TRUE.equals(tcpConfig.getSslEnabled())) { + .setReuseAddress(true) + .setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时 + if (Boolean.TRUE.equals(tcpConfig.getSslEnabled())) { PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() .setKeyPath(tcpConfig.getSslKeyPath()) .setCertPath(tcpConfig.getSslCertPath()); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java index 76e6c4b250..6e15e95a21 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; @@ -27,6 +28,11 @@ import org.springframework.util.Assert; @Slf4j public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { + /** + * 最大记录大小(64KB),防止 DoS 攻击 + */ + private static final int MAX_RECORD_SIZE = 65536; + /** * 解析后的分隔符字节数组 */ @@ -45,6 +51,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { @Override public RecordParser createDecodeParser(Handler handler) { RecordParser parser = RecordParser.newDelimited(Buffer.buffer(delimiterBytes)); + parser.maxRecordSize(MAX_RECORD_SIZE); // 设置最大记录大小,防止 DoS 攻击 // 处理完整消息(不包含分隔符) parser.handler(handler); parser.exceptionHandler(ex -> { @@ -76,7 +83,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { .replace("\\r", "\r") .replace("\\n", "\n") .replace("\\t", "\t"); - return parsed.getBytes(); + return StrUtil.utf8Bytes(parsed); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java index 092b485be8..eda77c4d59 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java @@ -46,6 +46,11 @@ public class IotTcpFixedLengthFrameCodec implements IotTcpFrameCodec { @Override public Buffer encode(byte[] data) { + // 校验数据长度不能超过固定长度 + if (data.length > fixedLength) { + throw new IllegalArgumentException(String.format( + "数据长度 %d 超过固定长度 %d", data.length, fixedLength)); + } Buffer buffer = Buffer.buffer(fixedLength); buffer.appendBytes(data); // 如果数据不足固定长度,填充 0(RecordParser.newFixed 解码时按固定长度读取,所以发送端需要填充) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java index 2fb05cdd42..980a32650e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java @@ -25,7 +25,7 @@ import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; import org.springframework.util.Assert; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; /** @@ -95,8 +95,8 @@ public class IotTcpUpstreamHandler implements Handler { try { processMessage(clientId, buffer, socket); } catch (Exception e) { - log.error("[handle][消息处理失败,客户端 ID: {},地址: {},错误: {}]", - clientId, socket.remoteAddress(), e.getMessage()); + log.error("[handle][消息处理失败,客户端 ID: {},地址: {}]", + clientId, socket.remoteAddress(), e); socket.close(); } }; @@ -114,20 +114,40 @@ public class IotTcpUpstreamHandler implements Handler { * @param socket 网络连接 */ private void processMessage(String clientId, Buffer buffer, NetSocket socket) { - // 1. 反序列化消息 - IotDeviceMessage message = serializer.deserialize(buffer.getBytes()); - Assert.notNull(message, "反序列化后消息为空"); + IotDeviceMessage message = null; + try { + // 1. 反序列化消息 + message = serializer.deserialize(buffer.getBytes()); + if (message == null) { + sendErrorResponse(socket, null, null, BAD_REQUEST.getCode(), "消息反序列化失败"); + return; + } - // 2. 根据消息类型路由处理 - if (AUTH_METHOD.equals(message.getMethod())) { - // 认证请求 - handleAuthenticationRequest(clientId, message, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(clientId, message, socket); - } else { - // 业务消息 - handleBusinessRequest(clientId, message, socket); + // 2. 根据消息类型路由处理 + if (AUTH_METHOD.equals(message.getMethod())) { + // 认证请求 + handleAuthenticationRequest(clientId, message, socket); + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { + // 设备动态注册请求 + handleRegisterRequest(clientId, message, socket); + } else { + // 业务消息 + handleBusinessRequest(clientId, message, socket); + } + } catch (IllegalArgumentException e) { + // 参数校验失败,返回 400 + log.warn("[processMessage][参数校验失败,客户端 ID: {},错误: {}]", clientId, e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage()); + } catch (Exception e) { + // 其他异常,返回 500 并重新抛出让上层关闭连接 + log.error("[processMessage][处理消息失败,客户端 ID: {}]", clientId, e); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + throw e; } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java index 2197f98f6e..e58e369509 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java @@ -21,6 +21,11 @@ import java.util.concurrent.ConcurrentHashMap; @Slf4j public class IotTcpConnectionManager { + /** + * 最大连接数 + */ + private final int maxConnections; + /** * 连接信息映射:NetSocket -> 连接信息 */ @@ -31,6 +36,24 @@ public class IotTcpConnectionManager { */ private final Map deviceSocketMap = new ConcurrentHashMap<>(); + public IotTcpConnectionManager(int maxConnections) { + this.maxConnections = maxConnections; + } + + /** + * 获取当前连接数 + */ + public int getConnectionCount() { + return connectionMap.size(); + } + + /** + * 检查是否可以接受新连接 + */ + public boolean canAcceptConnection() { + return connectionMap.size() < maxConnections; + } + /** * 注册设备连接(包含认证信息) * @@ -39,6 +62,10 @@ public class IotTcpConnectionManager { * @param connectionInfo 连接信息 */ public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { + // 检查连接数是否已达上限 + if (connectionMap.size() >= maxConnections) { + throw new IllegalStateException("连接数已达上限: " + maxConnections); + } // 如果设备已有其他连接,先清理旧连接 NetSocket oldSocket = deviceSocketMap.get(deviceId); if (oldSocket != null && oldSocket != socket) { @@ -67,7 +94,8 @@ public class IotTcpConnectionManager { return; } Long deviceId = connectionInfo.getDeviceId(); - deviceSocketMap.remove(deviceId); + // 仅当 deviceSocketMap 中的 socket 是当前 socket 时才移除,避免误删新连接 + deviceSocketMap.remove(deviceId, socket); log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress()); } From cb301eb788704a62a21b490f265c97bf07f7d1a4 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 11:21:12 +0800 Subject: [PATCH 13/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91udp=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=8850%=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E4=B8=BB=E6=B5=81=E7=A8=8B=E8=B7=91=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gateway/codec/simple/package-info.java | 4 - .../config/IotGatewayConfiguration.java | 49 +- .../gateway/config/IotGatewayProperties.java | 52 +- .../gateway/protocol/IotProtocolManager.java | 40 +- .../protocol/http/IotHttpProtocol.java | 4 +- .../gateway/protocol/tcp/IotTcpProtocol.java | 18 +- .../upstream/IotTcpUpstreamHandler.java | 16 +- .../tcp/manager/IotTcpConnectionManager.java | 19 - .../gateway/protocol/udp/IotUdpConfig.java | 49 ++ .../gateway/protocol/udp/IotUdpProtocol.java | 259 +++++++++ .../protocol/udp/IotUdpUpstreamProtocol.java | 196 ------- .../downstream}/IotUdpDownstreamHandler.java | 51 +- .../IotUdpDownstreamSubscriber.java | 6 +- .../upstream/IotUdpUpstreamHandler.java | 395 +++++++++++++ .../udp/manager/IotUdpSessionManager.java | 189 +++--- .../udp/router/IotUdpUpstreamHandler.java | 542 ------------------ .../src/main/resources/application.yaml | 28 +- ...irectDeviceUdpProtocolIntegrationTest.java | 177 ++---- ...tewayDeviceUdpProtocolIntegrationTest.java | 231 +++----- ...aySubDeviceUdpProtocolIntegrationTest.java | 137 ++--- 20 files changed, 1098 insertions(+), 1364 deletions(-) delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/{router => handler/downstream}/IotUdpDownstreamHandler.java (50%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/{ => handler/downstream}/IotUdpDownstreamSubscriber.java (80%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java deleted file mode 100644 index 5bd676ad1a..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * TODO @芋艿:实现一个 alink 的 xml 版本 - */ -package cn.iocoder.yudao.module.iot.gateway.codec.simple; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index 3aaf3b1d2a..c2bf96df32 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -11,10 +11,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; @@ -42,10 +38,8 @@ public class IotGatewayConfiguration { } @Bean - public IotProtocolManager iotProtocolManager(IotGatewayProperties gatewayProperties, - IotMessageSerializerManager serializerManager, - IotMessageBus messageBus) { - return new IotProtocolManager(gatewayProperties, serializerManager, messageBus); + public IotProtocolManager iotProtocolManager(IotGatewayProperties gatewayProperties) { + return new IotProtocolManager(gatewayProperties); } /** @@ -117,45 +111,6 @@ public class IotGatewayConfiguration { } - /** - * IoT 网关 UDP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true") - @Slf4j - public static class UdpProtocolConfiguration { - - @Bean(name = "udpVertx", destroyMethod = "close") - public Vertx udpVertx() { - return Vertx.vertx(); - } - - @Bean - public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotUdpSessionManager sessionManager, - @Qualifier("udpVertx") Vertx udpVertx) { - return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(), - deviceService, messageService, sessionManager, udpVertx); - } - - @Bean - public IotUdpDownstreamHandler iotUdpDownstreamHandler(IotDeviceMessageService messageService, - IotUdpSessionManager sessionManager, - IotUdpUpstreamProtocol protocol) { - return new IotUdpDownstreamHandler(messageService, sessionManager, protocol); - } - - @Bean - public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler, - IotUdpDownstreamHandler downstreamHandler, - IotMessageBus messageBus) { - return new IotUdpDownstreamSubscriber(protocolHandler, downstreamHandler, messageBus); - } - - } - /** * IoT 网关 CoAP 协议配置类 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 707c8e37e0..1f0dccdaf2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.config; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; import io.vertx.core.net.KeyCertOptions; import io.vertx.core.net.TrustOptions; import jakarta.validation.Valid; @@ -35,7 +36,7 @@ public class IotGatewayProperties { private ProtocolProperties protocol; /** - * 协议实例列表(新版) + * 协议实例列表 */ private List protocols; @@ -89,11 +90,6 @@ public class IotGatewayProperties { */ private MqttProperties mqtt; - /** - * UDP 组件配置 - */ - private UdpProperties udp; - /** * CoAP 组件配置 */ @@ -348,44 +344,6 @@ public class IotGatewayProperties { } - @Data - public static class UdpProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务端口(默认 8093) - */ - private Integer port = 8093; - - /** - * 接收缓冲区大小(默认 64KB) - */ - private Integer receiveBufferSize = 65536; - - /** - * 发送缓冲区大小(默认 64KB) - */ - private Integer sendBufferSize = 65536; - - /** - * 会话超时时间(毫秒,默认 60 秒) - *

                  - * 用于清理不活跃的设备地址映射 - */ - private Long sessionTimeoutMs = 60000L; - - /** - * 会话清理间隔(毫秒,默认 30 秒) - */ - private Long sessionCleanIntervalMs = 30000L; - - } - @Data public static class CoapProperties { @@ -525,6 +483,12 @@ public class IotGatewayProperties { @Valid private IotTcpConfig tcp; + /** + * UDP 协议配置 + */ + @Valid + private IotUdpConfig udp; + } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index c64c828f8d..c13b44ee5f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.iot.gateway.protocol; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.BooleanUtil; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; -import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.context.SmartLifecycle; @@ -24,20 +24,16 @@ public class IotProtocolManager implements SmartLifecycle { private final IotGatewayProperties gatewayProperties; - private final IotMessageSerializerManager serializerManager; - - private final IotMessageBus messageBus; - + /** + * 协议实例列表 + */ private final List protocols = new ArrayList<>(); + @Getter private volatile boolean running = false; - public IotProtocolManager(IotGatewayProperties gatewayProperties, - IotMessageSerializerManager serializerManager, - IotMessageBus messageBus) { + public IotProtocolManager(IotGatewayProperties gatewayProperties) { this.gatewayProperties = gatewayProperties; - this.serializerManager = serializerManager; - this.messageBus = messageBus; } @Override @@ -84,11 +80,6 @@ public class IotProtocolManager implements SmartLifecycle { log.info("[stop][协议管理器已停止]"); } - @Override - public boolean isRunning() { - return running; - } - /** * 创建协议实例 * @@ -107,7 +98,8 @@ public class IotProtocolManager implements SmartLifecycle { return createHttpProtocol(config); case TCP: return createTcpProtocol(config); - // TODO 后续添加其他协议类型 + case UDP: + return createUdpProtocol(config); default: throw new IllegalArgumentException(String.format( "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); @@ -121,7 +113,7 @@ public class IotProtocolManager implements SmartLifecycle { * @return HTTP 协议实例 */ private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { - return new IotHttpProtocol(config, messageBus); + return new IotHttpProtocol(config); } /** @@ -131,7 +123,17 @@ public class IotProtocolManager implements SmartLifecycle { * @return TCP 协议实例 */ private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { - return new IotTcpProtocol(config, messageBus, serializerManager); + return new IotTcpProtocol(config); + } + + /** + * 创建 UDP 协议实例 + * + * @param config 协议实例配置 + * @return UDP 协议实例 + */ + private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + return new IotUdpProtocol(config); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java index 51665e8f46..f3a3c0d14d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; @@ -59,11 +60,12 @@ public class IotHttpProtocol implements IotProtocol { */ private IotHttpDownstreamSubscriber downstreamSubscriber; - public IotHttpProtocol(ProtocolInstanceProperties properties, IotMessageBus messageBus) { + public IotHttpProtocol(ProtocolInstanceProperties properties) { this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java index d1f894e1a9..7fe3e519c0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; @@ -73,31 +74,30 @@ public class IotTcpProtocol implements IotProtocol { private final IotTcpFrameCodec frameCodec; /** - * TCP 连接管理器(每个 Protocol 实例独立) + * TCP 连接管理器 */ private final IotTcpConnectionManager connectionManager; - public IotTcpProtocol(ProtocolInstanceProperties properties, IotMessageBus messageBus, - IotMessageSerializerManager serializerManager) { + public IotTcpProtocol(ProtocolInstanceProperties properties) { + IotTcpConfig tcpConfig = properties.getTcp(); + Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); + Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); // 初始化序列化器 IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); - if (serializeType == null) { - serializeType = IotSerializeTypeEnum.JSON; // 默认 JSON - } + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); this.serializer = serializerManager.get(serializeType); // 初始化帧编解码器 - IotTcpConfig tcpConfig = properties.getTcp(); - Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); - Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); this.frameCodec = IotTcpFrameCodecFactory.create(tcpConfig.getCodec()); // 初始化连接管理器 this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections()); // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java index 980a32650e..fe3a38fe50 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java @@ -49,12 +49,10 @@ public class IotTcpUpstreamHandler implements Handler { */ private final IotMessageSerializer serializer; /** - * TCP 连接管理器(每个 Protocol 实例独立) + * TCP 连接管理器 */ private final IotTcpConnectionManager connectionManager; - // ===================== Spring 依赖(构造时注入) ===================== - private final IotDeviceMessageService deviceMessageService; private final IotDeviceService deviceService; private final IotDeviceCommonApi deviceApi; @@ -141,7 +139,7 @@ public class IotTcpUpstreamHandler implements Handler { String method = message != null ? message.getMethod() : null; sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage()); } catch (Exception e) { - // 其他异常,返回 500 并重新抛出让上层关闭连接 + // 其他异常,返回 500,并重新抛出让上层关闭连接 log.error("[processMessage][处理消息失败,客户端 ID: {}]", clientId, e); String requestId = message != null ? message.getRequestId() : null; String method = message != null ? message.getMethod() : null; @@ -158,6 +156,7 @@ public class IotTcpUpstreamHandler implements Handler { * @param message 消息信息 * @param socket 网络连接 */ + @SuppressWarnings("DuplicatedCode") private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, NetSocket socket) { // 1. 解析认证参数 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); @@ -180,7 +179,7 @@ public class IotTcpUpstreamHandler implements Handler { Assert.notNull(device, "设备不存在"); // 3.1 注册连接 - registerConnection(socket, device, clientId); + registerConnection(socket, device); // 3.2 发送上线消息 sendOnlineMessage(device); // 3.3 发送成功响应 @@ -196,6 +195,7 @@ public class IotTcpUpstreamHandler implements Handler { * @param socket 网络连接 * @see 阿里云 - 一型一密 */ + @SuppressWarnings("DuplicatedCode") private void handleRegisterRequest(String clientId, IotDeviceMessage message, NetSocket socket) { // 1. 解析注册参数 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); @@ -246,14 +246,12 @@ public class IotTcpUpstreamHandler implements Handler { * * @param socket 网络连接 * @param device 设备 - * @param clientId 客户端 ID */ - private void registerConnection(NetSocket socket, IotDeviceRespDTO device, String clientId) { + private void registerConnection(NetSocket socket, IotDeviceRespDTO device) { IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo() .setDeviceId(device.getId()) .setProductKey(device.getProductKey()) - .setDeviceName(device.getDeviceName()) - .setClientId(clientId); + .setDeviceName(device.getDeviceName()); connectionManager.registerConnection(socket, device.getId(), connectionInfo); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java index e58e369509..36c5928762 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java @@ -40,20 +40,6 @@ public class IotTcpConnectionManager { this.maxConnections = maxConnections; } - /** - * 获取当前连接数 - */ - public int getConnectionCount() { - return connectionMap.size(); - } - - /** - * 检查是否可以接受新连接 - */ - public boolean canAcceptConnection() { - return connectionMap.size() < maxConnections; - } - /** * 注册设备连接(包含认证信息) * @@ -155,11 +141,6 @@ public class IotTcpConnectionManager { */ private String deviceName; - /** - * 客户端 ID - */ - private String clientId; - } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java new file mode 100644 index 0000000000..74b9f4cefb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.udp; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT UDP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotUdpConfig { + + /** + * 最大会话数 + */ + @NotNull(message = "最大会话数不能为空") + @Min(value = 1, message = "最大会话数必须大于 0") + private Integer maxSessions = 1000; + /** + * 会话超时时间(毫秒) + *

                  + * 用于清理不活跃的设备地址映射 + */ + @NotNull(message = "会话超时时间不能为空") + @Min(value = 1000, message = "会话超时时间必须大于 1000 毫秒") + private Long sessionTimeoutMs = 60000L; + /** + * 会话清理间隔(毫秒) + */ + @NotNull(message = "会话清理间隔不能为空") + @Min(value = 1000, message = "会话清理间隔必须大于 1000 毫秒") + private Long sessionCleanIntervalMs = 30000L; + + /** + * 接收缓冲区大小(字节) + */ + @NotNull(message = "接收缓冲区大小不能为空") + @Min(value = 1024, message = "接收缓冲区大小必须大于 1024 字节") + private Integer receiveBufferSize = 65536; + /** + * 发送缓冲区大小(字节) + */ + @NotNull(message = "发送缓冲区大小不能为空") + @Min(value = 1024, message = "发送缓冲区大小必须大于 1024 字节") + private Integer sendBufferSize = 65536; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java new file mode 100644 index 0000000000..463b72b7ac --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java @@ -0,0 +1,259 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.udp; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream.IotUdpUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +import io.vertx.core.datagram.DatagramSocket; +import io.vertx.core.datagram.DatagramSocketOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; + +import java.util.List; + +/** + * IoT UDP 协议实现 + *

                  + * 基于 Vert.x 实现 UDP 服务器,接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotUdpProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolInstanceProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * UDP 服务器 + */ + @Getter + private DatagramSocket udpSocket; + + /** + * 下行消息订阅者 + */ + private final IotUdpDownstreamSubscriber downstreamSubscriber; + + /** + * 消息序列化器 + */ + private final IotMessageSerializer serializer; + + /** + * UDP 会话管理器 + */ + private final IotUdpSessionManager sessionManager; + + private final IotDeviceService deviceService; + private final IotDeviceMessageService deviceMessageService; + + /** + * 会话清理定时器 ID + */ + // TODO @AI:会话清理,是不是放到 sessionManager 更合适? + private Long cleanTimerId; + + public IotUdpProtocol(ProtocolInstanceProperties properties) { + IotUdpConfig udpConfig = properties.getUdp(); + Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + + // 初始化序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); + this.serializer = serializerManager.get(serializeType); + + // 初始化会话管理器 + this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions()); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotUdpDownstreamHandler downstreamHandler = new IotUdpDownstreamHandler(this, sessionManager, serializer); + this.downstreamSubscriber = new IotUdpDownstreamSubscriber(this, downstreamHandler, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.UDP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT UDP 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + + // 1.2 创建 UDP Socket 选项 + IotUdpConfig udpConfig = properties.getUdp(); + DatagramSocketOptions options = new DatagramSocketOptions() + .setReceiveBufferSize(udpConfig.getReceiveBufferSize()) + .setSendBufferSize(udpConfig.getSendBufferSize()) + .setReuseAddress(true); + + // 1.3 创建 UDP Socket + udpSocket = vertx.createDatagramSocket(options); + + // 1.4 创建上行消息处理器 + IotUdpUpstreamHandler upstreamHandler = new IotUdpUpstreamHandler(serverId, sessionManager, serializer); + + // 1.5 监听端口 + udpSocket.listen(properties.getPort(), "0.0.0.0", result -> { + if (result.failed()) { + log.error("[start][IoT UDP 协议 {} 启动失败]", getId(), result.cause()); + return; + } + // 设置数据包处理器 + udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); + running = true; + log.info("[start][IoT UDP 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + // 启动会话清理定时器 + startSessionCleanTimer(udpConfig); + + // 2. 启动下行消息订阅者 + // TODO @AI:这里会导致 Thread Thread[vert.x-eventloop-thread-0,5,main] has been blocked for 2992 ms, time limit is 2000 ms + this.downstreamSubscriber.start(); + }); + } + + @Override + public void stop() { + if (!running) { + return; + } + // 1. 停止下行消息订阅者 + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT UDP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + + // 2.1 取消会话清理定时器 + if (cleanTimerId != null) { + vertx.cancelTimer(cleanTimerId); + cleanTimerId = null; + log.info("[stop][IoT UDP 协议 {} 会话清理定时器已取消]", getId()); + } + // 2.2 关闭 UDP Socket + if (udpSocket != null) { + try { + udpSocket.close().result(); + log.info("[stop][IoT UDP 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT UDP 协议 {} 服务器停止失败]", getId(), e); + } + udpSocket = null; + } + // 2.3 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT UDP 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT UDP 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT UDP 协议 {} 已停止]", getId()); + } + + /** + * 启动会话清理定时器 + */ + // TODO @AI:这个放到 + private void startSessionCleanTimer(IotUdpConfig udpConfig) { + cleanTimerId = vertx.setPeriodic(udpConfig.getSessionCleanIntervalMs(), id -> { + try { + // 1. 清理超时的设备会话,并获取离线设备列表 + List offlineDeviceIds = sessionManager.cleanExpiredSessions(udpConfig.getSessionTimeoutMs()); + + // 2. 为每个离线设备发送离线消息 + for (Long deviceId : offlineDeviceIds) { + sendOfflineMessage(deviceId); + } + if (CollUtil.isNotEmpty(offlineDeviceIds)) { + log.info("[cleanExpiredSessions][本次清理 {} 个超时设备]", offlineDeviceIds.size()); + } + } catch (Exception e) { + log.error("[cleanExpiredSessions][清理超时会话失败]", e); + } + }); + log.info("[startSessionCleanTimer][会话清理定时器启动,间隔:{} ms,超时:{} ms]", + udpConfig.getSessionCleanIntervalMs(), udpConfig.getSessionTimeoutMs()); + } + + /** + * 发送设备离线消息 + * + * @param deviceId 设备 ID + */ + private void sendOfflineMessage(Long deviceId) { + try { + // 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceId); + if (device == null) { + log.warn("[sendOfflineMessage][设备不存在,设备 ID: {}]", deviceId); + return; + } + + // 发送离线消息 + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + log.info("[sendOfflineMessage][发送离线消息,设备 ID: {},设备名: {}]", + deviceId, device.getDeviceName()); + } catch (Exception e) { + log.error("[sendOfflineMessage][发送离线消息失败,设备 ID: {}]", deviceId, e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java deleted file mode 100644 index 294368f291..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java +++ /dev/null @@ -1,196 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.core.datagram.DatagramSocket; -import io.vertx.core.datagram.DatagramSocketOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; - -/** - * IoT 网关 UDP 协议:接收设备上行消息 - *

                  - * 采用 Vertx DatagramSocket 实现 UDP 服务器,主要功能: - * 1. 监听 UDP 端口,接收设备消息 - * 2. 定期清理不活跃的设备地址映射 - * 3. 提供 UDP Socket 用于下行消息发送 - * - * @author 芋道源码 - */ -@Slf4j -public class IotUdpUpstreamProtocol implements IotProtocol { - - private static final String ID = "udp"; - - private final IotGatewayProperties.UdpProperties udpProperties; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotUdpSessionManager sessionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - @Getter - private DatagramSocket udpSocket; - - /** - * 会话清理定时器 ID - */ - private Long cleanTimerId; - - private IotUdpUpstreamHandler upstreamHandler; - - private volatile boolean running = false; - - public IotUdpUpstreamProtocol(IotGatewayProperties.UdpProperties udpProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotUdpSessionManager sessionManager, - Vertx vertx) { - this.udpProperties = udpProperties; - this.deviceService = deviceService; - this.messageService = messageService; - this.sessionManager = sessionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(udpProperties.getPort()); - } - - @Override - public String getId() { - return ID; - } - - @Override - public IotProtocolTypeEnum getType() { - return IotProtocolTypeEnum.UDP; - } - - @Override - @PostConstruct - public void start() { - // 1. 初始化上行消息处理器 - this.upstreamHandler = new IotUdpUpstreamHandler(this, messageService, deviceService, sessionManager); - - // 2. 创建 UDP Socket 选项 - DatagramSocketOptions options = new DatagramSocketOptions() - .setReceiveBufferSize(udpProperties.getReceiveBufferSize()) - .setSendBufferSize(udpProperties.getSendBufferSize()) - .setReuseAddress(true); - - // 3. 创建 UDP Socket - udpSocket = vertx.createDatagramSocket(options); - - // 4. 监听端口 - udpSocket.listen(udpProperties.getPort(), "0.0.0.0", result -> { - if (result.failed()) { - log.error("[start][IoT 网关 UDP 协议启动失败]", result.cause()); - return; - } - // 设置数据包处理器 - udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); - running = true; - log.info("[start][IoT 网关 UDP 协议启动成功,端口:{},接收缓冲区:{} 字节,发送缓冲区:{} 字节]", - udpProperties.getPort(), udpProperties.getReceiveBufferSize(), - udpProperties.getSendBufferSize()); - - // 5. 启动会话清理定时器 - startSessionCleanTimer(); - }); - } - - @Override - @PreDestroy - public void stop() { - // 1. 取消会话清理定时器 - if (cleanTimerId != null) { - vertx.cancelTimer(cleanTimerId); - cleanTimerId = null; - log.info("[stop][会话清理定时器已取消]"); - } - - // 2. 关闭 UDP Socket - if (udpSocket != null) { - try { - udpSocket.close().result(); - running = false; - log.info("[stop][IoT 网关 UDP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 UDP 协议停止失败]", e); - } - } - } - - @Override - public boolean isRunning() { - return running; - } - - /** - * 启动会话清理定时器 - */ - private void startSessionCleanTimer() { - cleanTimerId = vertx.setPeriodic(udpProperties.getSessionCleanIntervalMs(), id -> { - try { - // 1. 清理超时的设备地址映射,并获取离线设备列表 - List offlineDeviceIds = sessionManager.cleanExpiredMappings(udpProperties.getSessionTimeoutMs()); - - // 2. 为每个离线设备发送离线消息 - for (Long deviceId : offlineDeviceIds) { - sendOfflineMessage(deviceId); - } - if (CollUtil.isNotEmpty(offlineDeviceIds)) { - log.info("[cleanExpiredMappings][本次清理 {} 个超时设备]", offlineDeviceIds.size()); - } - } catch (Exception e) { - log.error("[cleanExpiredMappings][清理超时会话失败]", e); - } - }); - log.info("[startSessionCleanTimer][会话清理定时器启动,间隔:{} ms,超时:{} ms]", - udpProperties.getSessionCleanIntervalMs(), udpProperties.getSessionTimeoutMs()); - } - - /** - * 发送设备离线消息 - * - * @param deviceId 设备 ID - */ - private void sendOfflineMessage(Long deviceId) { - try { - // 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceId); - if (device == null) { - log.warn("[sendOfflineMessage][设备不存在,设备 ID: {}]", deviceId); - return; - } - - // 发送离线消息 - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - messageService.sendDeviceMessage(offlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - log.info("[sendOfflineMessage][发送离线消息,设备 ID: {},设备名: {}]", - deviceId, device.getDeviceName()); - } catch (Exception e) { - log.error("[sendOfflineMessage][发送离线消息失败,设备 ID: {}]", deviceId, e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java similarity index 50% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java index 6aeb2cb7aa..dbe465d1ee 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java @@ -1,10 +1,11 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import io.vertx.core.datagram.DatagramSocket; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** @@ -13,54 +14,48 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j +@RequiredArgsConstructor public class IotUdpDownstreamHandler { - private final IotDeviceMessageService deviceMessageService; + private final IotUdpProtocol protocol; private final IotUdpSessionManager sessionManager; - private final IotUdpUpstreamProtocol protocol; - - public IotUdpDownstreamHandler(IotDeviceMessageService deviceMessageService, - IotUdpSessionManager sessionManager, - IotUdpUpstreamProtocol protocol) { - this.deviceMessageService = deviceMessageService; - this.sessionManager = sessionManager; - this.protocol = protocol; - } + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; /** * 处理下行消息 - * - * @param message 下行消息 */ public void handle(IotDeviceMessage message) { try { log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", message.getDeviceId(), message.getMethod(), message.getId()); - - // 1. 获取会话信息(包含 codecType) + // 1. 检查设备会话 IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId()); if (sessionInfo == null) { - log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId()); + log.warn("[handle][会话信息不存在,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); return; } - - // 2. 使用会话中的 codecType 编码消息,并发送到设备 - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, sessionInfo.getCodecType()); DatagramSocket socket = protocol.getUdpSocket(); if (socket == null) { log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId()); return; } - boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes, socket); - if (success) { - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); - } else { - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); + + // 2. 序列化消息 + byte[] serializedData = serializer.serialize(message); + + // 3. 发送到设备 + boolean success = sessionManager.sendToDevice(message.getDeviceId(), serializedData, socket); + if (!success) { + throw new RuntimeException("下行消息发送失败"); } + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), serializedData.length); } catch (Exception e) { log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", message.getDeviceId(), message.getMethod(), message, e); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java similarity index 80% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java index e11380c73b..ea0bc99b39 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java @@ -1,9 +1,9 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler; import lombok.extern.slf4j.Slf4j; /** @@ -16,7 +16,7 @@ public class IotUdpDownstreamSubscriber extends IotProtocolDownstreamSubscriber private final IotUdpDownstreamHandler downstreamHandler; - public IotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocol, + public IotUdpDownstreamSubscriber(IotProtocol protocol, IotUdpDownstreamHandler downstreamHandler, IotMessageBus messageBus) { super(protocol, messageBus); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java new file mode 100644 index 0000000000..3e1a4fe143 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java @@ -0,0 +1,395 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.datagram.DatagramPacket; +import io.vertx.core.datagram.DatagramSocket; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; + +import java.net.InetSocketAddress; +import java.time.LocalDateTime; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; + +/** + * UDP 上行消息处理器 + *

                  + * 采用无状态 Token 机制(每次请求携带 token): + * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password + * 2. 返回 Token:服务端验证后返回 JWT token + * 3. 后续请求:每次请求在 params 中携带 token + * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证 + * + * @author 芋道源码 + */ +@Slf4j +public class IotUdpUpstreamHandler { + + private static final String AUTH_METHOD = "auth"; + + /** + * Token 参数 Key + */ + private static final String PARAM_KEY_TOKEN = "token"; + /** + * Body 参数 Key(实际请求内容) + */ + private static final String PARAM_KEY_BODY = "body"; + + private final String serverId; + + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + /** + * UDP 会话管理器 + */ + private final IotUdpSessionManager sessionManager; + + private final IotDeviceMessageService deviceMessageService; + private final IotDeviceService deviceService; + private final IotDeviceTokenService deviceTokenService; + private final IotDeviceCommonApi deviceApi; + + public IotUdpUpstreamHandler(String serverId, + IotUdpSessionManager sessionManager, + IotMessageSerializer serializer) { + Assert.notNull(serializer, "消息序列化器必须配置"); + Assert.notNull(sessionManager, "会话管理器不能为空"); + this.serverId = serverId; + this.sessionManager = sessionManager; + this.serializer = serializer; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + } + + // TODO @AI:vertx 有 udp 的实现么? + /** + * 处理 UDP 数据包 + * + * @param packet 数据包 + * @param socket UDP Socket + */ + public void handle(DatagramPacket packet, DatagramSocket socket) { + InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port()); + Buffer data = packet.data(); + String addressKey = sessionManager.buildAddressKey(senderAddress); + log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", addressKey, data.length()); + try { + processMessage(data, senderAddress, socket); + } catch (IllegalArgumentException e) { + // 参数校验失败,返回 400 + log.warn("[handle][参数校验失败,来源: {},错误: {}]", addressKey, e.getMessage()); + sendErrorResponse(socket, senderAddress, null, null, BAD_REQUEST.getCode(), e.getMessage()); + } catch (Exception e) { + // 其他异常,返回 500 + log.error("[handle][处理消息失败,来源: {}]", addressKey, e); + sendErrorResponse(socket, senderAddress, null, null, + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + } + } + + /** + * 处理消息 + * + * @param buffer 消息 + * @param senderAddress 发送者地址 + * @param socket UDP Socket + */ + private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { + // 1.1 基础检查 + if (ArrayUtil.isEmpty(buffer)) { + return; + } + // 1.2 反序列化消息 + IotDeviceMessage message = serializer.deserialize(buffer.getBytes()); + if (message == null) { + sendErrorResponse(socket, senderAddress, null, null, BAD_REQUEST.getCode(), "消息反序列化失败"); + return; + } + + // 2. 根据消息类型路由处理 + if (AUTH_METHOD.equals(message.getMethod())) { + // 认证请求 + handleAuthenticationRequest(message, senderAddress, socket); + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { + // 设备动态注册请求 + handleRegisterRequest(message, senderAddress, socket); + } else { + // 业务消息 + handleBusinessRequest(message, senderAddress, socket); + } + } + + /** + * 处理认证请求 + * + * @param message 消息信息 + * @param senderAddress 发送者地址 + * @param socket UDP Socket + */ + @SuppressWarnings("DuplicatedCode") + private void handleAuthenticationRequest(IotDeviceMessage message, InetSocketAddress senderAddress, + DatagramSocket socket) { + String clientId = IdUtil.simpleUUID(); + // 1. 解析认证参数 + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); + Assert.notNull(authParams, "认证参数不能为空"); + Assert.hasText(authParams.getUsername(), "username 不能为空"); + Assert.hasText(authParams.getPassword(), "password 不能为空"); + + // 2.1 执行认证 + CommonResult authResult = deviceApi.authDevice(authParams); + if (authResult.isError()) { + log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", + clientId, authParams.getUsername()); + sendErrorResponse(socket, senderAddress, message.getRequestId(), AUTH_METHOD, + authResult.getCode(), authResult.getMsg()); + return; + } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + + // 3. 生成 JWT Token(无状态) + String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName()); + + // 4.1 注册会话 + registerSession(senderAddress, device, clientId); + // 4.2 发送上线消息 + sendOnlineMessage(device); + // 4.3 发送成功响应(包含 token) + sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token); + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", + device.getId(), device.getDeviceName(), sessionManager.buildAddressKey(senderAddress)); + } + + /** + * 处理设备动态注册请求(一型一密,不需要认证) + * + * @param message 消息信息 + * @param senderAddress 发送者地址 + * @param socket UDP Socket + * @see 阿里云 - 一型一密 + */ + @SuppressWarnings("DuplicatedCode") + private void handleRegisterRequest(IotDeviceMessage message, InetSocketAddress senderAddress, + DatagramSocket socket) { + // 1. 解析注册参数 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); + Assert.notNull(params, "注册参数不能为空"); + Assert.hasText(params.getProductKey(), "productKey 不能为空"); + Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); + + // 2. 调用动态注册 + CommonResult result = deviceApi.registerDevice(params); + if (result.isError()) { + log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", + sessionManager.buildAddressKey(senderAddress), result.getMsg()); + sendErrorResponse(socket, senderAddress, message.getRequestId(), + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getCode(), result.getMsg()); + return; + } + + // 3. 发送成功响应 + sendSuccessResponse(socket, senderAddress, message.getRequestId(), + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); + log.info("[handleRegisterRequest][注册成功,来源: {},设备名: {}]", + sessionManager.buildAddressKey(senderAddress), params.getDeviceName()); + } + + /** + * 处理业务请求 + *

                  + * 请求参数格式: + * - token:JWT 令牌 + * - body:实际请求内容(可以是 Map、List 或其他类型) + * + * @param message 消息信息 + * @param senderAddress 发送者地址 + * @param socket UDP Socket + */ + @SuppressWarnings("unchecked") + private void handleBusinessRequest(IotDeviceMessage message, InetSocketAddress senderAddress, + DatagramSocket socket) { + String addressKey = sessionManager.buildAddressKey(senderAddress); + // 1.1 从消息中提取 token 和 body + String token = null; + Object body = null; + if (message.getParams() instanceof Map) { + Map paramsMap = (Map) message.getParams(); + token = (String) paramsMap.get(PARAM_KEY_TOKEN); + body = paramsMap.get(PARAM_KEY_BODY); + } + if (StrUtil.isBlank(token)) { + log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + UNAUTHORIZED.getCode(), "请先进行认证"); + return; + } + // 1.2 验证 token,获取设备信息 + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); + if (deviceInfo == null) { + log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + UNAUTHORIZED.getCode(), "token 无效或已过期"); + return; + } + // 1.3 获取设备详细信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + if (device == null) { + log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]", + addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + BAD_REQUEST.getCode(), "设备不存在"); + return; + } + + // 2. 更新会话活跃时间和地址 + // TODO @AI:是不是合并到 sessionManager 里面更好? + IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(device.getId()); + if (sessionInfo != null) { + // 检查地址是否变化,变化则更新 + if (!senderAddress.equals(sessionInfo.getAddress())) { + sessionManager.updateSessionAddress(device.getId(), senderAddress); + } else { + sessionManager.updateSessionActivity(device.getId()); + } + } + + // 3. 将 body 设置为实际的 params,发送消息到消息总线 + message.setParams(body); + deviceMessageService.sendDeviceMessage(message, device.getProductKey(), + device.getDeviceName(), serverId); + log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]", + device.getId(), message.getMethod(), addressKey); + } + + /** + * 注册会话信息 + * + * @param address 设备地址 + * @param device 设备 + * @param clientId 客户端 ID + */ + private void registerSession(InetSocketAddress address, IotDeviceRespDTO device, String clientId) { + IotUdpSessionManager.SessionInfo sessionInfo = new IotUdpSessionManager.SessionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()) + .setAddress(address) + .setLastActiveTime(LocalDateTime.now()); + sessionManager.registerSession(device.getId(), sessionInfo); + } + + /** + * 发送设备上线消息 + * + * @param device 设备信息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + } + + // ===================== 发送响应消息 ===================== + + /** + * 发送认证成功响应(包含 token) + * + * @param socket UDP Socket + * @param address 目标地址 + * @param requestId 请求 ID + * @param token JWT Token + */ + private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address, + String requestId, String token) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, token, + SUCCESS.getCode(), null); + writeResponse(socket, address, responseMessage); + } + + /** + * 发送成功响应 + * + * @param socket UDP Socket + * @param address 目标地址 + * @param requestId 请求 ID + * @param method 方法名 + * @param data 响应数据 + */ + private void sendSuccessResponse(DatagramSocket socket, InetSocketAddress address, + String requestId, String method, Object data) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null); + writeResponse(socket, address, responseMessage); + } + + /** + * 发送错误响应 + * + * @param socket UDP Socket + * @param address 目标地址 + * @param requestId 请求 ID + * @param method 方法名 + * @param code 错误码 + * @param msg 错误消息 + */ + private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address, + String requestId, String method, Integer code, String msg) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg); + writeResponse(socket, address, responseMessage); + } + + /** + * 写入响应到 Socket + * + * @param socket UDP Socket + * @param address 目标地址 + * @param responseMessage 响应消息 + */ + private void writeResponse(DatagramSocket socket, InetSocketAddress address, IotDeviceMessage responseMessage) { + try { + byte[] serializedData = serializer.serialize(responseMessage); + socket.send(Buffer.buffer(serializedData), address.getPort(), address.getHostString(), result -> { + if (result.failed()) { + log.error("[writeResponse][发送响应失败,地址: {}]", + sessionManager.buildAddressKey(address), result.cause()); + } + }); + } catch (Exception e) { + log.error("[writeResponse][发送响应异常,地址: {}]", + sessionManager.buildAddressKey(address), e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java index 79a5bf0245..5280250331 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java @@ -4,7 +4,6 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.datagram.DatagramSocket; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.net.InetSocketAddress; import java.time.LocalDateTime; @@ -17,91 +16,122 @@ import java.util.concurrent.ConcurrentHashMap; /** * IoT 网关 UDP 会话管理器 *

                  - * 采用无状态设计,SessionManager 主要用于: - * 1. 管理设备地址映射(用于下行消息发送) - * 2. 定期清理不活跃的设备地址映射 - *

                  - * 注意:UDP 是无连接协议,上行消息通过 token 验证身份,不依赖会话状态 + * 统一管理 UDP 会话的认证状态、设备会话和消息发送功能: + * 1. 管理 UDP 会话的认证状态 + * 2. 管理设备会话和在线状态 + * 3. 管理消息发送到设备 * * @author 芋道源码 */ @Slf4j -@Component public class IotUdpSessionManager { /** - * 设备 ID -> 会话信息(包含地址和 codecType) + * 最大会话数 + */ + private final int maxSessions; + + /** + * 设备 ID -> 会话信息 */ private final Map deviceSessionMap = new ConcurrentHashMap<>(); - /** - * 设备地址 Key -> 最后活跃时间(用于清理) - */ - private final Map lastActiveTimeMap = new ConcurrentHashMap<>(); - /** * 设备地址 Key -> 设备 ID(反向映射,用于清理时同步) */ + // TODO @AI:1)这个变量是否必须?2)unregisterSession 这个方法是否必须? private final Map addressDeviceMap = new ConcurrentHashMap<>(); + public IotUdpSessionManager(int maxSessions) { + this.maxSessions = maxSessions; + } + /** - * 更新设备会话(每次收到上行消息时调用) + * 注册设备会话(包含认证信息) * - * @param deviceId 设备 ID - * @param address 设备地址 - * @param codecType 消息编解码类型 + * @param deviceId 设备 ID + * @param sessionInfo 会话信息 */ - public void updateDeviceSession(Long deviceId, InetSocketAddress address, String codecType) { - String addressKey = buildAddressKey(address); - // 更新设备会话映射 - deviceSessionMap.put(deviceId, new SessionInfo().setAddress(address).setCodecType(codecType)); - lastActiveTimeMap.put(addressKey, LocalDateTime.now()); + public void registerSession(Long deviceId, SessionInfo sessionInfo) { + // 检查会话数是否已达上限 + if (deviceSessionMap.size() >= maxSessions) { + throw new IllegalStateException("会话数已达上限: " + maxSessions); + } + // 如果设备已有其他会话,先清理旧会话 + SessionInfo oldSessionInfo = deviceSessionMap.get(deviceId); + if (oldSessionInfo != null) { + String oldAddressKey = buildAddressKey(oldSessionInfo.getAddress()); + addressDeviceMap.remove(oldAddressKey, deviceId); + log.info("[registerSession][设备已有其他会话,清理旧会话,设备 ID: {},旧地址: {}]", + deviceId, oldAddressKey); + } + + // 注册新会话 + String addressKey = buildAddressKey(sessionInfo.getAddress()); + deviceSessionMap.put(deviceId, sessionInfo); addressDeviceMap.put(addressKey, deviceId); - log.debug("[updateDeviceSession][更新设备会话,设备 ID: {},地址: {},codecType: {}]", deviceId, addressKey, codecType); + log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},product key: {},device name: {}]", + deviceId, addressKey, sessionInfo.getProductKey(), sessionInfo.getDeviceName()); } /** - * 更新设备地址(兼容旧接口,默认不更新 codecType) + * 注销设备会话 * * @param deviceId 设备 ID - * @param address 设备地址 */ - public void updateDeviceAddress(Long deviceId, InetSocketAddress address) { - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); - String codecType = sessionInfo != null ? sessionInfo.getCodecType() : null; - updateDeviceSession(deviceId, address, codecType); + public void unregisterSession(Long deviceId) { + SessionInfo sessionInfo = deviceSessionMap.remove(deviceId); + if (sessionInfo == null) { + return; + } + String addressKey = buildAddressKey(sessionInfo.getAddress()); + // 仅当 addressDeviceMap 中的 deviceId 是当前 deviceId 时才移除,避免误删新会话 + addressDeviceMap.remove(addressKey, deviceId); + log.info("[unregisterSession][注销设备会话,设备 ID: {},地址: {}]", deviceId, addressKey); } /** - * 获取设备会话信息 + * 更新会话活跃时间(每次收到上行消息时调用) * * @param deviceId 设备 ID - * @return 会话信息 + */ + public void updateSessionActivity(Long deviceId) { + SessionInfo sessionInfo = deviceSessionMap.get(deviceId); + if (sessionInfo != null) { + sessionInfo.setLastActiveTime(LocalDateTime.now()); + } + } + + /** + * 更新设备会话地址(设备地址变更时调用) + * + * @param deviceId 设备 ID + * @param newAddress 新地址 + */ + public void updateSessionAddress(Long deviceId, InetSocketAddress newAddress) { + SessionInfo sessionInfo = deviceSessionMap.get(deviceId); + if (sessionInfo == null) { + return; + } + // 清理旧地址映射 + String oldAddressKey = buildAddressKey(sessionInfo.getAddress()); + addressDeviceMap.remove(oldAddressKey, deviceId); + + // 更新新地址 + String newAddressKey = buildAddressKey(newAddress); + sessionInfo.setAddress(newAddress); + sessionInfo.setLastActiveTime(LocalDateTime.now()); + addressDeviceMap.put(newAddressKey, deviceId); + log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},新地址: {}]", deviceId, newAddressKey); + } + + /** + * 获取会话信息 */ public SessionInfo getSessionInfo(Long deviceId) { return deviceSessionMap.get(deviceId); } - /** - * 检查设备是否在线(即是否有地址映射) - * - * @param deviceId 设备 ID - * @return 是否在线 - */ - public boolean isDeviceOnline(Long deviceId) { - return deviceSessionMap.containsKey(deviceId); - } - - /** - * 检查设备是否离线 - * - * @param deviceId 设备 ID - * @return 是否离线 - */ - public boolean isDeviceOffline(Long deviceId) { - return !isDeviceOnline(deviceId); - } - /** * 发送消息到设备 * @@ -116,17 +146,16 @@ public class IotUdpSessionManager { log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId); return false; } - InetSocketAddress address = sessionInfo.getAddress(); try { socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { if (result.succeeded()) { log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", deviceId, buildAddressKey(address), data.length); - } else { - log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", - deviceId, buildAddressKey(address), result.cause()); + return; } + log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", + deviceId, buildAddressKey(address), result.cause()); }); return true; } catch (Exception e) { @@ -136,37 +165,31 @@ public class IotUdpSessionManager { } /** - * 定期清理不活跃的设备地址映射 + * 定期清理不活跃的设备会话 * * @param timeoutMs 超时时间(毫秒) * @return 清理的设备 ID 列表(用于发送离线消息) */ - public List cleanExpiredMappings(long timeoutMs) { + public List cleanExpiredSessions(long timeoutMs) { List offlineDeviceIds = new ArrayList<>(); LocalDateTime now = LocalDateTime.now(); LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000); - Iterator> iterator = lastActiveTimeMap.entrySet().iterator(); + Iterator> iterator = deviceSessionMap.entrySet().iterator(); + // TODO @AI:改成 for each 会不会更好? while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + SessionInfo sessionInfo = entry.getValue(); // 未过期,跳过 - Map.Entry entry = iterator.next(); - if (entry.getValue().isAfter(expireTime)) { + if (sessionInfo.getLastActiveTime().isAfter(expireTime)) { continue; } // 过期处理:记录离线设备 ID - String addressKey = entry.getKey(); - Long deviceId = addressDeviceMap.remove(addressKey); - if (deviceId == null) { - iterator.remove(); - continue; - } - SessionInfo sessionInfo = deviceSessionMap.remove(deviceId); - if (sessionInfo == null) { - iterator.remove(); - continue; - } + Long deviceId = entry.getKey(); + String addressKey = buildAddressKey(sessionInfo.getAddress()); + addressDeviceMap.remove(addressKey, deviceId); offlineDeviceIds.add(deviceId); - log.debug("[cleanExpiredMappings][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]", - deviceId, addressKey, entry.getValue()); + log.debug("[cleanExpiredSessions][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]", + deviceId, addressKey, sessionInfo.getLastActiveTime()); iterator.remove(); } return offlineDeviceIds; @@ -183,20 +206,32 @@ public class IotUdpSessionManager { } /** - * 会话信息 + * 会话信息(包含认证信息) */ @Data public static class SessionInfo { + /** + * 设备 ID + */ + private Long deviceId; + /** + * 产品 Key + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; + /** * 设备地址 */ private InetSocketAddress address; - /** - * 消息编解码类型 + * 最后活跃时间 */ - private String codecType; + private LocalDateTime lastActiveTime; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java deleted file mode 100644 index 872a615a6f..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java +++ /dev/null @@ -1,542 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.datagram.DatagramPacket; -import io.vertx.core.datagram.DatagramSocket; -import lombok.extern.slf4j.Slf4j; - -import java.net.InetSocketAddress; -import java.util.Map; - -/** - * UDP 上行消息处理器 - *

                  - * 采用无状态 Token 机制(每次请求携带 token): - * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password - * 2. 返回 Token:服务端验证后返回 JWT token - * 3. 后续请求:每次请求在 params 中携带 token - * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证 - * - * @author 芋道源码 - */ -@Slf4j -public class IotUdpUpstreamHandler { - - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; - - private static final String AUTH_METHOD = "auth"; - /** - * Token 参数 Key - */ - private static final String PARAM_KEY_TOKEN = "token"; - /** - * Body 参数 Key(实际请求内容) - */ - private static final String PARAM_KEY_BODY = "body"; - - private final IotDeviceMessageService deviceMessageService; - - private final IotDeviceService deviceService; - - private final IotUdpSessionManager sessionManager; - - private final IotDeviceTokenService deviceTokenService; - - private final IotDeviceCommonApi deviceApi; - - private final String serverId; - - public IotUdpUpstreamHandler(IotUdpUpstreamProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotDeviceService deviceService, - IotUdpSessionManager sessionManager) { - this.deviceMessageService = deviceMessageService; - this.deviceService = deviceService; - this.sessionManager = sessionManager; - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.serverId = protocol.getServerId(); - } - - /** - * 处理 UDP 数据包 - * - * @param packet 数据包 - * @param socket UDP Socket - */ - public void handle(DatagramPacket packet, DatagramSocket socket) { - InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port()); - Buffer data = packet.data(); - log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", - sessionManager.buildAddressKey(senderAddress), data.length()); - try { - processMessage(data, senderAddress, socket); - } catch (Exception e) { - log.error("[handle][处理消息失败,来源: {},错误: {}]", - sessionManager.buildAddressKey(senderAddress), e.getMessage(), e); - // UDP 无连接,不需要断开连接,只记录错误 - } - } - - /** - * 处理消息 - * - * @param buffer 消息 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - */ - private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { - // 1. 基础检查 - if (buffer == null || buffer.length() == 0) { - return; - } - - // 2. 获取消息格式类型 - String codecType = getMessageCodecType(buffer); - - // 3. 解码消息 - IotDeviceMessage message; - try { - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); - if (message == null) { - log.warn("[processMessage][消息解码失败,来源: {}]", sessionManager.buildAddressKey(senderAddress)); - sendErrorResponse(socket, senderAddress, null, "消息解码失败", codecType); - return; - } - } catch (Exception e) { - log.error("[processMessage][消息解码异常,来源: {}]", sessionManager.buildAddressKey(senderAddress), e); - sendErrorResponse(socket, senderAddress, null, "消息解码失败: " + e.getMessage(), codecType); - return; - } - - // 4. 根据消息类型路由处理 - try { - if (AUTH_METHOD.equals(message.getMethod())) { - // 认证请求 - handleAuthenticationRequest(message, codecType, senderAddress, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(message, codecType, senderAddress, socket); - } else { - // 业务消息 - handleBusinessRequest(message, codecType, senderAddress, socket); - } - } catch (Exception e) { - log.error("[processMessage][处理消息失败,来源: {},消息方法: {}]", - sessionManager.buildAddressKey(senderAddress), message.getMethod(), e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "消息处理失败", codecType); - } - } - - /** - * 处理认证请求 - * - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - */ - private void handleAuthenticationRequest(IotDeviceMessage message, String codecType, - InetSocketAddress senderAddress, DatagramSocket socket) { - String addressKey = sessionManager.buildAddressKey(senderAddress); - try { - // 1.1 解析认证参数 - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); - if (authParams == null) { - log.warn("[handleAuthenticationRequest][认证参数解析失败,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证参数不完整", codecType); - return; - } - // 1.2 执行认证 - if (!validateDeviceAuth(authParams)) { - log.warn("[handleAuthenticationRequest][认证失败,来源: {},username: {}]", - addressKey, authParams.getUsername()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证失败", codecType); - return; - } - - // 2.1 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); - if (deviceInfo == null) { - sendErrorResponse(socket, senderAddress, message.getRequestId(), "解析设备信息失败", codecType); - return; - } - // 2.2 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType); - return; - } - - // 3.1 生成 JWT Token(无状态) - String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName()); - - // 3.2 更新设备会话信息(用于下行消息,保存 codecType) - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); - - // 3.3 发送上线消息 - sendOnlineMessage(device); - - // 3.4 发送成功响应(包含 token) - sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token, codecType); - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", - device.getId(), device.getDeviceName(), addressKey); - } catch (Exception e) { - log.error("[handleAuthenticationRequest][认证处理异常,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证处理异常", codecType); - } - } - - /** - * 处理设备动态注册请求(一型一密,不需要 Token) - * - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - * @see 阿里云 - 一型一密 - */ - private void handleRegisterRequest(IotDeviceMessage message, String codecType, - InetSocketAddress senderAddress, DatagramSocket socket) { - String addressKey = sessionManager.buildAddressKey(senderAddress); - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null) { - log.warn("[handleRegisterRequest][注册参数解析失败,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册参数不完整", codecType); - return; - } - - // 2. 调用动态注册 - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", addressKey, result.getMsg()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), result.getMsg(), codecType); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(socket, senderAddress, message.getRequestId(), result.getData(), codecType); - log.info("[handleRegisterRequest][注册成功,设备名: {},来源: {}]", - params.getDeviceName(), addressKey); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册处理异常", codecType); - } - } - - /** - * 处理业务请求 - *

                  - * 请求参数格式: - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - */ - @SuppressWarnings("unchecked") - private void handleBusinessRequest(IotDeviceMessage message, String codecType, - InetSocketAddress senderAddress, DatagramSocket socket) { - String addressKey = sessionManager.buildAddressKey(senderAddress); - try { - // 1.1 从消息中提取 token 和 body(格式:{token: "xxx", body: {...}} 或 {token: "xxx", body: [...]}) - String token = null; - Object body = null; - if (message.getParams() instanceof Map) { - Map paramsMap = (Map) message.getParams(); - token = (String) paramsMap.get(PARAM_KEY_TOKEN); - body = paramsMap.get(PARAM_KEY_BODY); - } - if (StrUtil.isBlank(token)) { - log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "请先进行认证", codecType); - return; - } - // 1.2 验证 token,获取设备信息 - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); - if (deviceInfo == null) { - log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "token 无效或已过期", codecType); - return; - } - - // 2. 获取设备详细信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]", - addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType); - return; - } - - // 3. 更新设备会话信息(保持最新,保存 codecType) - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); - - // 4. 将 body 设置为实际的 params,发送消息到消息总线 - message.setParams(body); - deviceMessageService.sendDeviceMessage(message, device.getProductKey(), - device.getDeviceName(), serverId); - log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]", - device.getId(), message.getMethod(), addressKey); - } catch (Exception e) { - log.error("[handleBusinessRequest][业务请求处理异常,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "处理失败", codecType); - } - } - - /** - * 获取消息编解码类型 - * - * @param buffer 消息 - * @return 消息编解码类型 - */ - private String getMessageCodecType(Buffer buffer) { - // 检测消息格式类型 - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY - : CODEC_TYPE_JSON; - } - - /** - * 发送设备上线消息 - * - * @param device 设备信息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); - } - } - - /** - * 验证设备认证信息 - * - * @param authParams 认证参数 - * @return 是否认证成功 - */ - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) - .setPassword(authParams.getPassword())); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); - return false; - } - } - - /** - * 发送认证成功响应(包含 token) - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param token JWT Token - * @param codecType 消息编解码类型 - */ - private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, String token, String codecType) { - try { - // 构建响应数据 - Object responseData = MapUtil.builder() - .put("success", true) - .put("token", token) - .put("message", "认证成功") - .build(); - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, 0, "认证成功"); - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - // 发送响应 - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { - if (result.failed()) { - log.error("[sendAuthSuccessResponse][发送认证成功响应失败,地址: {}]", - sessionManager.buildAddressKey(address), result.cause()); - } - }); - } catch (Exception e) { - log.error("[sendAuthSuccessResponse][发送认证成功响应异常,地址: {}]", - sessionManager.buildAddressKey(address), e); - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param registerResp 注册响应 - * @param codecType 消息编解码类型 - */ - private void sendRegisterSuccessResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, IotDeviceRegisterRespDTO registerResp, - String codecType) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - // 2. 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { - if (result.failed()) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,地址: {}]", - sessionManager.buildAddressKey(address), result.cause()); - } - }); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,地址: {}]", - sessionManager.buildAddressKey(address), e); - } - } - - /** - * 发送错误响应 - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - * @param codecType 消息编解码类型 - */ - private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, String errorMessage, String codecType) { - sendResponse(socket, address, false, errorMessage, requestId, codecType); - } - - /** - * 发送响应消息 - * - * @param socket UDP Socket - * @param address 目标地址 - * @param success 是否成功 - * @param message 消息 - * @param requestId 请求 ID - * @param codecType 消息编解码类型 - */ - @SuppressWarnings("SameParameterValue") - private void sendResponse(DatagramSocket socket, InetSocketAddress address, boolean success, - String message, String requestId, String codecType) { - try { - // 构建响应数据 - Object responseData = MapUtil.builder() - .put("success", success) - .put("message", message) - .build(); - int code = success ? 0 : 401; - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - "response", responseData, code, message); - - // 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), ar -> { - if (ar.failed()) { - log.error("[sendResponse][发送响应失败,地址: {}]", - sessionManager.buildAddressKey(address), ar.cause()); - } - }); - } catch (Exception e) { - log.error("[sendResponse][发送响应异常,地址: {}]", - sessionManager.buildAddressKey(address), e); - } - } - - /** - * 解析认证参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 认证参数 DTO,解析失败时返回 null - */ - @SuppressWarnings("unchecked") - private IotDeviceAuthReqDTO parseAuthParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceAuthReqDTO() - .setClientId(MapUtil.getStr(paramMap, "clientId")) - .setUsername(MapUtil.getStr(paramMap, "username")) - .setPassword(MapUtil.getStr(paramMap, "password")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceAuthReqDTO) { - return (IotDeviceAuthReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); - } catch (Exception e) { - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); - return null; - } - } - - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceRegisterReqDTO() - .setProductKey(MapUtil.getStr(paramMap, "productKey")) - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceRegisterReqDTO) { - return (IotDeviceRegisterReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index c5c8d52b09..49ab8e2c0a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -50,7 +50,7 @@ yudao: - id: http-json type: http port: 8092 - enabled: true + enabled: false http: ssl-enabled: false # ==================================== @@ -59,7 +59,7 @@ yudao: - id: tcp-json type: tcp port: 8091 - enabled: true + enabled: false serialize: json tcp: max-connections: 1000 @@ -75,6 +75,20 @@ yudao: # initial-bytes-to-strip: 4 # 初始跳过的字节数 # type: fixed_length # 拆包类型:length_field / delimiter / fixed_length # fixed-length: 256 # 固定长度 + # ==================================== + # 针对引入的 UDP 组件的配置 + # ==================================== + - id: udp-json + type: udp + port: 8093 + enabled: true + serialize: json + udp: + max-sessions: 1000 # 最大会话数 + session-timeout-ms: 60000 # 会话超时时间(毫秒) + session-clean-interval-ms: 30000 # 会话清理间隔(毫秒) + receive-buffer-size: 65536 # 接收缓冲区大小(字节) + send-buffer-size: 65536 # 发送缓冲区大小(字节) # 协议配置(旧版,保持兼容) protocol: @@ -113,16 +127,6 @@ yudao: trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 # ==================================== - # 针对引入的 UDP 组件的配置 - # ==================================== - udp: - enabled: false # 是否启用 UDP - port: 8093 # UDP 服务端口 - receive-buffer-size: 65536 # 接收缓冲区大小(字节,默认 64KB) - send-buffer-size: 65536 # 发送缓冲区大小(字节,默认 64KB) - session-timeout-ms: 60000 # 会话超时时间(毫秒,默认 60 秒) - session-clean-interval-ms: 30000 # 会话清理间隔(毫秒,默认 30 秒) - # ==================================== # 针对引入的 MQTT 组件的配置 # ==================================== mqtt: diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java index 9d507cc036..0942be32c1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -9,9 +8,8 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -27,16 +25,9 @@ import java.util.Map; * *

                  测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 UDP 协议直接连接平台 * - *

                  支持两种编解码格式: - *

                    - *
                  • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
                  • - *
                  • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
                  • - *
                  - * *

                  使用步骤: *

                    *
                  1. 启动 yudao-module-iot-gateway 服务(UDP 端口 8093)
                  2. - *
                  3. 修改 {@link #CODEC} 选择测试的编解码格式
                  4. *
                  5. 运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
                  6. *
                  7. 运行以下测试方法: *
                      @@ -58,10 +49,12 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { private static final int SERVER_PORT = 8093; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -81,30 +74,18 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); - } else { - log.warn("[testAuth][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testAuth][响应消息: {}]", response); + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); } // ===================== 动态注册测试 ===================== @@ -118,30 +99,18 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testDeviceRegister() throws Exception { - // 1.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-udp-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + // 1. 构建注册消息 + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName("test-udp-" + System.currentTimeMillis()) + .setProductSecret("test-product-secret"); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testDeviceRegister][响应消息: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - } else { - log.warn("[testDeviceRegister][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testDeviceRegister][响应消息: {}]", response); + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); } // ===================== 直连设备属性上报测试 ===================== @@ -151,31 +120,17 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - // 1.1 构建属性上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 1. 构建属性上报消息(UDP 协议:token 放在 params 中) + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), withToken(IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("width", 1) .put("height", "2") - .build())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + .build()))); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testPropertyPost][响应消息: {}]", response); } // ===================== 直连设备事件上报测试 ===================== @@ -185,31 +140,17 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - // 1.1 构建事件上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 1. 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), withToken(IotDeviceEventPostReqDTO.of( "eat", MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + System.currentTimeMillis()))); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testEventPost][响应消息: {}]", response); } // ===================== 辅助方法 ===================== @@ -232,30 +173,36 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { } /** - * 发送 UDP 请求并接收响应 + * 发送 UDP 消息并接收响应 * - * @param socket UDP Socket - * @param payload 请求数据 - * @return 响应数据 + * @param request 请求消息 + * @return 响应消息 */ - public static byte[] sendAndReceive(DatagramSocket socket, byte[] payload) throws Exception { - InetAddress address = InetAddress.getByName(SERVER_HOST); + private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception { + // 1. 序列化请求 + byte[] payload = SERIALIZER.serialize(request); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length); - // 发送请求 - DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); - socket.send(sendPacket); + // 2. 发送请求 + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(TIMEOUT_MS); + InetAddress address = InetAddress.getByName(SERVER_HOST); + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); + socket.send(sendPacket); - // 接收响应 - byte[] receiveData = new byte[4096]; - DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); - try { - socket.receive(receivePacket); - byte[] response = new byte[receivePacket.getLength()]; - System.arraycopy(receivePacket.getData(), 0, response, 0, receivePacket.getLength()); - return response; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; + // 3. 接收响应 + byte[] receiveData = new byte[4096]; + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + try { + socket.receive(receivePacket); + byte[] responseBytes = new byte[receivePacket.getLength()]; + System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength()); + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", responseBytes.length); + return SERIALIZER.deserialize(responseBytes); + } catch (java.net.SocketTimeoutException e) { + log.warn("[sendAndReceive][接收响应超时]"); + return null; + } } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java index 20c2933a0d..0acdeae38a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -13,36 +12,27 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.net.DatagramPacket; import java.net.DatagramSocket; +import java.net.InetAddress; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; -import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive; - /** * IoT 网关设备 UDP 协议集成测试(手动测试) * *

                      测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 UDP 协议管理子设备拓扑关系 * - *

                      支持两种编解码格式: - *

                        - *
                      • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
                      • - *
                      • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
                      • - *
                      - * *

                      使用步骤: *

                        *
                      1. 启动 yudao-module-iot-gateway 服务(UDP 端口 8093)
                      2. - *
                      3. 修改 {@link #CODEC} 选择测试的编解码格式
                      4. *
                      5. 运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量
                      6. *
                      7. 运行以下测试方法: *
                          @@ -63,12 +53,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd @Disabled public class IotGatewayDeviceUdpProtocolIntegrationTest { + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 8093; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== @@ -93,185 +87,101 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); - } else { - log.warn("[testAuth][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testAuth][响应消息: {}]", response); + log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); } // ===================== 拓扑管理测试 ===================== /** * 添加子设备拓扑关系测试 - *

                          - * 网关设备向平台上报需要绑定的子设备信息 */ @Test public void testTopoAdd() throws Exception { - // 1.1 构建子设备认证信息 + // 1. 构建子设备认证信息 IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() .setClientId(subAuthInfo.getClientId()) .setUsername(subAuthInfo.getUsername()) .setPassword(subAuthInfo.getPassword()); - // 1.2 构建请求参数 IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - withToken(params), - null, null, null); - // 1.3 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), withToken(params)); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoAdd][响应消息: {}]", response); - } else { - log.warn("[testTopoAdd][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testTopoAdd][响应消息: {}]", response); } /** * 删除子设备拓扑关系测试 - *

                          - * 网关设备向平台上报需要解绑的子设备信息 */ @Test public void testTopoDelete() throws Exception { - // 1.1 构建请求参数 + // 1. 构建请求参数 IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); params.setSubDevices(Collections.singletonList( new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), - withToken(params), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), withToken(params)); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoDelete][响应消息: {}]", response); - } else { - log.warn("[testTopoDelete][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testTopoDelete][响应消息: {}]", response); } /** * 获取子设备拓扑关系测试 - *

                          - * 网关设备向平台查询已绑定的子设备列表 */ @Test public void testTopoGet() throws Exception { - // 1.1 构建请求参数(目前为空,预留扩展) + // 1. 构建请求参数 IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - withToken(params), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), withToken(params)); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoGet][响应消息: {}]", response); - } else { - log.warn("[testTopoGet][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testTopoGet][响应消息: {}]", response); } // ===================== 子设备注册测试 ===================== /** * 子设备动态注册测试 - *

                          - * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret - *

                          - * 注意:此接口需要网关 Token 认证 */ @Test public void testSubDeviceRegister() throws Exception { - // 1.1 构建请求参数 + // 1. 构建请求参数 IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); subDevice.setDeviceName("mougezishebei"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - withToken(Collections.singletonList(subDevice)), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); + withToken(Collections.singletonList(subDevice))); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testSubDeviceRegister][响应消息: {}]", response); - } else { - log.warn("[testSubDeviceRegister][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testSubDeviceRegister][响应消息: {}]", response); } // ===================== 批量上报测试 ===================== /** * 批量上报属性测试(网关 + 子设备) - *

                          - * 网关设备批量上报自身属性、事件,以及子设备的属性、事件 */ @Test public void testPropertyPackPost() throws Exception { @@ -307,40 +217,18 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { params.setProperties(gatewayProperties); params.setEvents(gatewayEvents); params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - withToken(params), - null, null, null); - // 1.7 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), withToken(params)); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPackPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPackPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testPropertyPackPost][响应消息: {}]", response); } // ===================== 辅助方法 ===================== /** * 构建带 token 的 params - *

                          - * 返回格式:{token: "xxx", body: params} - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param params 原始参数(Map、List 或对象) - * @return 包含 token 和 body 的 Map */ private Map withToken(Object params) { Map result = new HashMap<>(); @@ -349,4 +237,31 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { return result; } + /** + * 发送 UDP 消息并接收响应 + */ + private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception { + byte[] payload = SERIALIZER.serialize(request); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length); + + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(TIMEOUT_MS); + InetAddress address = InetAddress.getByName(SERVER_HOST); + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); + socket.send(sendPacket); + + byte[] receiveData = new byte[4096]; + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + try { + socket.receive(receivePacket); + byte[] responseBytes = new byte[receivePacket.getLength()]; + System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength()); + return SERIALIZER.deserialize(responseBytes); + } catch (java.net.SocketTimeoutException e) { + log.warn("[sendAndReceive][接收响应超时]"); + return null; + } + } + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java index 100c276de2..202713b629 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java @@ -1,26 +1,24 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.net.DatagramPacket; import java.net.DatagramSocket; +import java.net.InetAddress; import java.util.HashMap; import java.util.Map; -import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive; - /** * IoT 网关子设备 UDP 协议集成测试(手动测试) * @@ -29,17 +27,10 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd *

                          重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 *

                          网关设备转发子设备请求时,Token 使用子设备自己的信息。 * - *

                          支持两种编解码格式: - *

                            - *
                          • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
                          • - *
                          • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
                          • - *
                          - * *

                          使用步骤: *

                            *
                          1. 启动 yudao-module-iot-gateway 服务(UDP 端口 8093)
                          2. *
                          3. 确保子设备已通过 {@link IotGatewayDeviceUdpProtocolIntegrationTest#testTopoAdd()} 绑定到网关
                          4. - *
                          5. 修改 {@link #CODEC} 选择测试的编解码格式
                          6. *
                          7. 运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
                          8. *
                          9. 运行以下测试方法: *
                              @@ -57,12 +48,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd @Disabled public class IotGatewaySubDeviceUdpProtocolIntegrationTest { + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 8093; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -82,30 +77,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); - } else { - log.warn("[testAuth][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testAuth][响应消息: {}]", response); + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); } // ===================== 子设备属性上报测试 ===================== @@ -115,33 +98,19 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - // 1.1 构建属性上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 1. 构建属性上报消息(UDP 协议:token 放在 params 中) + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), withToken(IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("power", 100) .put("status", "online") .put("temperature", 36.5) - .build())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + .build()))); log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testPropertyPost][响应消息: {}]", response); } // ===================== 子设备事件上报测试 ===================== @@ -151,9 +120,8 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - // 1.1 构建事件上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 1. 构建事件上报消息(UDP 协议:token 放在 params 中) + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), withToken(IotDeviceEventPostReqDTO.of( "alarm", @@ -163,38 +131,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { .put("threshold", 40) .put("current", 42) .build(), - System.currentTimeMillis())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + System.currentTimeMillis()))); log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testEventPost][响应消息: {}]", response); } // ===================== 辅助方法 ===================== /** * 构建带 token 的 params - *

                              - * 返回格式:{token: "xxx", body: params} - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param params 原始参数(Map、List 或对象) - * @return 包含 token 和 body 的 Map */ private Map withToken(Object params) { Map result = new HashMap<>(); @@ -203,4 +151,31 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { return result; } + /** + * 发送 UDP 消息并接收响应 + */ + private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception { + byte[] payload = SERIALIZER.serialize(request); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length); + + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(TIMEOUT_MS); + InetAddress address = InetAddress.getByName(SERVER_HOST); + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); + socket.send(sendPacket); + + byte[] receiveData = new byte[4096]; + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + try { + socket.receive(receivePacket); + byte[] responseBytes = new byte[receivePacket.getLength()]; + System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength()); + return SERIALIZER.deserialize(responseBytes); + } catch (java.net.SocketTimeoutException e) { + log.warn("[sendAndReceive][接收响应超时]"); + return null; + } + } + } + } From 8e4b4cf20a05a16acb91146f5dacbfc569448357 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 12:51:06 +0800 Subject: [PATCH 14/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91udp=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=88100%=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E5=9F=BA=E4=BA=8E=20/Users/yunai/.claude/plans/iot-ud?= =?UTF-8?q?p-fix-plan.md=EF=BC=8C=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E4=BB=A5=E5=8F=8A=E5=90=84=E7=A7=8D=E7=BC=BA=E9=99=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gateway/protocol/tcp/IotTcpProtocol.java | 6 +- .../gateway/protocol/udp/IotUdpConfig.java | 8 +- .../gateway/protocol/udp/IotUdpProtocol.java | 108 ++++-------------- .../downstream/IotUdpDownstreamHandler.java | 2 +- .../src/main/resources/application.yaml | 3 +- ...irectDeviceUdpProtocolIntegrationTest.java | 4 +- ...aySubDeviceUdpProtocolIntegrationTest.java | 4 +- 7 files changed, 32 insertions(+), 103 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java index 7fe3e519c0..937745c584 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java @@ -155,7 +155,11 @@ public class IotTcpProtocol implements IotProtocol { this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT TCP 协议 {} 启动失败]", getId(), e); - // 启动失败时关闭 Vertx + // 启动失败时关闭资源 + if (tcpServer != null) { + tcpServer.close(); + tcpServer = null; + } if (vertx != null) { vertx.close(); vertx = null; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java index 74b9f4cefb..95a6291e4d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java @@ -21,17 +21,11 @@ public class IotUdpConfig { /** * 会话超时时间(毫秒) *

                              - * 用于清理不活跃的设备地址映射 + * 基于 Guava Cache 的 expireAfterAccess 实现自动过期清理 */ @NotNull(message = "会话超时时间不能为空") @Min(value = 1000, message = "会话超时时间必须大于 1000 毫秒") private Long sessionTimeoutMs = 60000L; - /** - * 会话清理间隔(毫秒) - */ - @NotNull(message = "会话清理间隔不能为空") - @Min(value = 1000, message = "会话清理间隔必须大于 1000 毫秒") - private Long sessionCleanIntervalMs = 30000L; /** * 接收缓冲区大小(字节) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java index 463b72b7ac..96f523dfd8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java @@ -1,13 +1,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; -import cn.hutool.core.collection.CollUtil; import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler; @@ -16,8 +12,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream.IotUdpU import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import io.vertx.core.Vertx; import io.vertx.core.datagram.DatagramSocket; import io.vertx.core.datagram.DatagramSocketOptions; @@ -25,8 +20,6 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.util.Assert; -import java.util.List; - /** * IoT UDP 协议实现 *

                              @@ -78,22 +71,11 @@ public class IotUdpProtocol implements IotProtocol { */ private final IotUdpSessionManager sessionManager; - private final IotDeviceService deviceService; - private final IotDeviceMessageService deviceMessageService; - - /** - * 会话清理定时器 ID - */ - // TODO @AI:会话清理,是不是放到 sessionManager 更合适? - private Long cleanTimerId; - public IotUdpProtocol(ProtocolInstanceProperties properties) { IotUdpConfig udpConfig = properties.getUdp(); Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); - this.deviceService = SpringUtil.getBean(IotDeviceService.class); - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); // 初始化序列化器 IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); @@ -102,7 +84,7 @@ public class IotUdpProtocol implements IotProtocol { this.serializer = serializerManager.get(serializeType); // 初始化会话管理器 - this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions()); + this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions(), udpConfig.getSessionTimeoutMs()); // 初始化下行消息订阅者 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); @@ -143,24 +125,30 @@ public class IotUdpProtocol implements IotProtocol { // 1.4 创建上行消息处理器 IotUdpUpstreamHandler upstreamHandler = new IotUdpUpstreamHandler(serverId, sessionManager, serializer); - // 1.5 监听端口 - udpSocket.listen(properties.getPort(), "0.0.0.0", result -> { - if (result.failed()) { - log.error("[start][IoT UDP 协议 {} 启动失败]", getId(), result.cause()); - return; - } + // 1.5 启动 UDP 服务器(阻塞式) + try { + udpSocket.listen(properties.getPort(), "0.0.0.0").result(); // 设置数据包处理器 udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); running = true; log.info("[start][IoT UDP 协议 {} 启动成功,端口:{},serverId:{}]", getId(), properties.getPort(), serverId); - // 启动会话清理定时器 - startSessionCleanTimer(udpConfig); // 2. 启动下行消息订阅者 - // TODO @AI:这里会导致 Thread Thread[vert.x-eventloop-thread-0,5,main] has been blocked for 2992 ms, time limit is 2000 ms this.downstreamSubscriber.start(); - }); + } catch (Exception e) { + log.error("[start][IoT UDP 协议 {} 启动失败]", getId(), e); + // 启动失败时关闭资源 + if (udpSocket != null) { + udpSocket.close(); + udpSocket = null; + } + if (vertx != null) { + vertx.close(); + vertx = null; + } + throw e; + } } @Override @@ -176,13 +164,7 @@ public class IotUdpProtocol implements IotProtocol { log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e); } - // 2.1 取消会话清理定时器 - if (cleanTimerId != null) { - vertx.cancelTimer(cleanTimerId); - cleanTimerId = null; - log.info("[stop][IoT UDP 协议 {} 会话清理定时器已取消]", getId()); - } - // 2.2 关闭 UDP Socket + // 2.1 关闭 UDP Socket if (udpSocket != null) { try { udpSocket.close().result(); @@ -192,7 +174,7 @@ public class IotUdpProtocol implements IotProtocol { } udpSocket = null; } - // 2.3 关闭 Vertx 实例 + // 2.2 关闭 Vertx 实例 if (vertx != null) { try { vertx.close().result(); @@ -206,54 +188,4 @@ public class IotUdpProtocol implements IotProtocol { log.info("[stop][IoT UDP 协议 {} 已停止]", getId()); } - /** - * 启动会话清理定时器 - */ - // TODO @AI:这个放到 - private void startSessionCleanTimer(IotUdpConfig udpConfig) { - cleanTimerId = vertx.setPeriodic(udpConfig.getSessionCleanIntervalMs(), id -> { - try { - // 1. 清理超时的设备会话,并获取离线设备列表 - List offlineDeviceIds = sessionManager.cleanExpiredSessions(udpConfig.getSessionTimeoutMs()); - - // 2. 为每个离线设备发送离线消息 - for (Long deviceId : offlineDeviceIds) { - sendOfflineMessage(deviceId); - } - if (CollUtil.isNotEmpty(offlineDeviceIds)) { - log.info("[cleanExpiredSessions][本次清理 {} 个超时设备]", offlineDeviceIds.size()); - } - } catch (Exception e) { - log.error("[cleanExpiredSessions][清理超时会话失败]", e); - } - }); - log.info("[startSessionCleanTimer][会话清理定时器启动,间隔:{} ms,超时:{} ms]", - udpConfig.getSessionCleanIntervalMs(), udpConfig.getSessionTimeoutMs()); - } - - /** - * 发送设备离线消息 - * - * @param deviceId 设备 ID - */ - private void sendOfflineMessage(Long deviceId) { - try { - // 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceId); - if (device == null) { - log.warn("[sendOfflineMessage][设备不存在,设备 ID: {}]", deviceId); - return; - } - - // 发送离线消息 - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - deviceMessageService.sendDeviceMessage(offlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - log.info("[sendOfflineMessage][发送离线消息,设备 ID: {},设备名: {}]", - deviceId, device.getDeviceName()); - } catch (Exception e) { - log.error("[sendOfflineMessage][发送离线消息失败,设备 ID: {}]", deviceId, e); - } - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java index dbe465d1ee..6caf71abec 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java @@ -34,7 +34,7 @@ public class IotUdpDownstreamHandler { log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", message.getDeviceId(), message.getMethod(), message.getId()); // 1. 检查设备会话 - IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId()); + IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSession(message.getDeviceId()); if (sessionInfo == null) { log.warn("[handle][会话信息不存在,设备 ID: {},方法: {},消息 ID: {}]", message.getDeviceId(), message.getMethod(), message.getId()); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 49ab8e2c0a..7691fc9b4a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -85,8 +85,7 @@ yudao: serialize: json udp: max-sessions: 1000 # 最大会话数 - session-timeout-ms: 60000 # 会话超时时间(毫秒) - session-clean-interval-ms: 30000 # 会话清理间隔(毫秒) + session-timeout-ms: 60000 # 会话超时时间(毫秒),基于 Guava Cache 自动过期 receive-buffer-size: 65536 # 接收缓冲区大小(字节) send-buffer-size: 65536 # 发送缓冲区大小(字节) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java index 0942be32c1..74169b2f12 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java @@ -65,7 +65,7 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { /** * 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 */ - private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTk0ODYzOCwiZGV2aWNlTmFtZSI6InNtYWxsIn0.TrOJisXhloZ3quLBOAIyowmpq6Syp9PHiEpfj-nQ9xo"; + private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc3MDUyNTA0MywiZGV2aWNlTmFtZSI6InNtYWxsIn0.W9Mo-Oe1ZNLDkINndKieUeW1XhDzhVp0W0zTAwO6hJM"; // ===================== 认证测试 ===================== @@ -120,7 +120,7 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - // 1. 构建属性上报消息(UDP 协议:token 放在 params 中) + // 1. 构建属性上报消息 IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), withToken(IotDevicePropertyPostReqDTO.of(MapUtil.builder() diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java index 202713b629..fe7f7f8126 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java @@ -98,7 +98,7 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - // 1. 构建属性上报消息(UDP 协议:token 放在 params 中) + // 1. 构建属性上报消息 IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), withToken(IotDevicePropertyPostReqDTO.of(MapUtil.builder() @@ -120,7 +120,7 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - // 1. 构建事件上报消息(UDP 协议:token 放在 params 中) + // 1. 构建事件上报消息 IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), withToken(IotDeviceEventPostReqDTO.of( From a28a15295cdb60f340f7157a5bbd18a76fd4ccbb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 17:05:02 +0800 Subject: [PATCH 15/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91coap=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=88100%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/IotGatewayConfiguration.java | 66 ------- .../gateway/config/IotGatewayProperties.java | 111 ++--------- .../gateway/protocol/IotProtocolManager.java | 26 +++ .../gateway/protocol/coap/IotCoapConfig.java | 36 ++++ .../protocol/coap/IotCoapProtocol.java | 173 ++++++++++++++++ .../coap/IotCoapUpstreamProtocol.java | 120 ----------- .../IotCoapDownstreamSubscriber.java | 5 +- .../upstrem/IotCoapAbstractHandler.java | 186 ++++++++++++++++++ .../handler/upstrem/IotCoapAuthHandler.java | 72 +++++++ .../upstrem}/IotCoapAuthResource.java | 10 +- .../upstrem/IotCoapRegisterHandler.java | 46 +++++ .../upstrem}/IotCoapRegisterResource.java | 2 +- .../upstrem/IotCoapRegisterSubHandler.java | 84 ++++++++ .../upstrem/IotCoapRegisterSubResource.java | 52 +++++ .../upstrem/IotCoapUpstreamHandler.java | 76 +++++++ .../IotCoapUpstreamTopicResource.java | 21 +- .../gateway/protocol/coap/package-info.java | 7 - .../coap/router/IotCoapAuthHandler.java | 115 ----------- .../coap/router/IotCoapRegisterHandler.java | 97 --------- .../coap/router/IotCoapUpstreamHandler.java | 110 ----------- .../protocol/coap/util/IotCoapUtils.java | 69 +------ .../upstream}/IotHttpAbstractHandler.java | 28 ++- .../handler/upstream/IotHttpAuthHandler.java | 19 +- .../upstream/IotHttpRegisterHandler.java | 17 +- .../upstream/IotHttpRegisterSubHandler.java | 9 +- .../upstream/IotHttpUpstreamHandler.java | 10 +- .../upstream/IotTcpUpstreamHandler.java | 25 +-- .../upstream/IotUdpUpstreamHandler.java | 111 +++++------ .../udp/manager/IotUdpSessionManager.java | 164 +++++---------- .../src/main/resources/application.yaml | 48 ++--- 30 files changed, 955 insertions(+), 960 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/{ => handler/downstream}/IotCoapDownstreamSubscriber.java (76%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAbstractHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthHandler.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/{router => handler/upstrem}/IotCoapAuthResource.java (64%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterHandler.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/{router => handler/upstrem}/IotCoapRegisterResource.java (92%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubResource.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamHandler.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/{router => handler/upstrem}/IotCoapUpstreamTopicResource.java (70%) delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/{router => handler/upstream}/IotHttpAbstractHandler.java (84%) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index c2bf96df32..e9800c34e4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -2,8 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.config; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; @@ -11,12 +9,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; import lombok.extern.slf4j.Slf4j; @@ -111,63 +104,4 @@ public class IotGatewayConfiguration { } - /** - * IoT 网关 CoAP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true") - @Slf4j - public static class CoapProtocolConfiguration { - - @Bean - public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) { - return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap()); - } - - @Bean - public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol, - IotMessageBus messageBus) { - return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus); - } - - } - - /** - * IoT 网关 WebSocket 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true") - @Slf4j - public static class WebSocketProtocolConfiguration { - - @Bean(name = "websocketVertx", destroyMethod = "close") - public Vertx websocketVertx() { - return Vertx.vertx(); - } - - @Bean - public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotWebSocketConnectionManager connectionManager, - @Qualifier("websocketVertx") Vertx websocketVertx) { - return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(), - deviceService, messageService, connectionManager, websocketVertx); - } - - @Bean - public IotWebSocketDownstreamHandler iotWebSocketDownstreamHandler(IotDeviceMessageService messageService, - IotWebSocketConnectionManager connectionManager) { - return new IotWebSocketDownstreamHandler(messageService, connectionManager); - } - - @Bean - public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler, - IotWebSocketDownstreamHandler downstreamHandler, - IotMessageBus messageBus) { - return new IotWebSocketDownstreamSubscriber(protocolHandler, downstreamHandler, messageBus); - } - - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 1f0dccdaf2..dc5d545373 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -1,9 +1,11 @@ package cn.iocoder.yudao.module.iot.gateway.config; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig; import io.vertx.core.net.KeyCertOptions; import io.vertx.core.net.TrustOptions; import jakarta.validation.Valid; @@ -90,16 +92,6 @@ public class IotGatewayProperties { */ private MqttProperties mqtt; - /** - * CoAP 组件配置 - */ - private CoapProperties coap; - - /** - * WebSocket 组件配置 - */ - private WebSocketProperties websocket; - } @Data @@ -344,93 +336,6 @@ public class IotGatewayProperties { } - @Data - public static class CoapProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务端口(CoAP 默认端口 5683) - */ - @NotNull(message = "服务端口不能为空") - private Integer port = 5683; - - /** - * 最大消息大小(字节) - */ - @NotNull(message = "最大消息大小不能为空") - private Integer maxMessageSize = 1024; - - /** - * ACK 超时时间(毫秒) - */ - @NotNull(message = "ACK 超时时间不能为空") - private Integer ackTimeout = 2000; - - /** - * 最大重传次数 - */ - @NotNull(message = "最大重传次数不能为空") - private Integer maxRetransmit = 4; - - } - - @Data - public static class WebSocketProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务器端口(默认:8094) - */ - private Integer port = 8094; - - /** - * WebSocket 路径(默认:/ws) - */ - @NotEmpty(message = "WebSocket 路径不能为空") - private String path = "/ws"; - - /** - * 最大消息大小(字节,默认 64KB) - */ - private Integer maxMessageSize = 65536; - - /** - * 最大帧大小(字节,默认 64KB) - */ - private Integer maxFrameSize = 65536; - - /** - * 空闲超时时间(秒,默认 60) - */ - private Integer idleTimeoutSeconds = 60; - - /** - * 是否启用 SSL(wss://) - */ - private Boolean sslEnabled = false; - - /** - * SSL 证书路径 - */ - private String sslCertPath; - - /** - * SSL 私钥路径 - */ - private String sslKeyPath; - - } - // TODO @AI:【暂时忽略】改成 ProtocolProperties /** * 协议实例配置 @@ -489,6 +394,18 @@ public class IotGatewayProperties { @Valid private IotUdpConfig udp; + /** + * CoAP 协议配置 + */ + @Valid + private IotCoapConfig coap; + + /** + * WebSocket 协议配置 + */ + @Valid + private IotWebSocketConfig websocket; + } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index c13b44ee5f..ed60897e55 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -4,9 +4,11 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.BooleanUtil; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.context.SmartLifecycle; @@ -100,6 +102,10 @@ public class IotProtocolManager implements SmartLifecycle { return createTcpProtocol(config); case UDP: return createUdpProtocol(config); + case COAP: + return createCoapProtocol(config); + case WEBSOCKET: + return createWebSocketProtocol(config); default: throw new IllegalArgumentException(String.format( "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); @@ -136,4 +142,24 @@ public class IotProtocolManager implements SmartLifecycle { return new IotUdpProtocol(config); } + /** + * 创建 CoAP 协议实例 + * + * @param config 协议实例配置 + * @return CoAP 协议实例 + */ + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + return new IotCoapProtocol(config); + } + + /** + * 创建 WebSocket 协议实例 + * + * @param config 协议实例配置 + * @return WebSocket 协议实例 + */ + private IotWebSocketUpstreamProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + return new IotWebSocketUpstreamProtocol(config); + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java new file mode 100644 index 0000000000..45fe3007e5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT CoAP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotCoapConfig { + + /** + * 最大消息大小(字节) + */ + @NotNull(message = "最大消息大小不能为空") + @Min(value = 64, message = "最大消息大小必须大于 64 字节") + private Integer maxMessageSize = 1024; + + /** + * ACK 超时时间(毫秒) + */ + @NotNull(message = "ACK 超时时间不能为空") + @Min(value = 100, message = "ACK 超时时间必须大于 100 毫秒") + private Integer ackTimeoutMs = 2000; + + /** + * 最大重传次数 + */ + @NotNull(message = "最大重传次数不能为空") + @Min(value = 0, message = "最大重传次数必须大于等于 0") + private Integer maxRetransmit = 4; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java new file mode 100644 index 0000000000..2749bd232c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java @@ -0,0 +1,173 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapAuthHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapAuthResource; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterResource; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterSubHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterSubResource; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapUpstreamTopicResource; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapResource; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.config.CoapConfig; +import org.eclipse.californium.elements.config.Configuration; +import org.springframework.util.Assert; + +import java.util.concurrent.TimeUnit; + +/** + * IoT CoAP 协议实现 + *

                              + * 基于 Eclipse Californium 实现,支持: + * 1. 认证:POST /auth + * 2. 设备动态注册:POST /auth/register/device + * 3. 子设备动态注册:POST /auth/register/sub-device/{productKey}/{deviceName} + * 4. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post + * 5. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolInstanceProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * CoAP 服务器 + */ + private CoapServer coapServer; + + /** + * 下行消息订阅者 + */ + private final IotCoapDownstreamSubscriber downstreamSubscriber; + + public IotCoapProtocol(ProtocolInstanceProperties properties) { + IotCoapConfig coapConfig = properties.getCoap(); + Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.COAP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT CoAP 协议 {} 已经在运行中]", getId()); + return; + } + + IotCoapConfig coapConfig = properties.getCoap(); + try { + // 1.1 创建 CoAP 配置 + Configuration config = Configuration.createStandardWithoutFile(); + config.set(CoapConfig.COAP_PORT, properties.getPort()); + config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize()); + config.set(CoapConfig.ACK_TIMEOUT, coapConfig.getAckTimeoutMs(), TimeUnit.MILLISECONDS); + config.set(CoapConfig.MAX_RETRANSMIT, coapConfig.getMaxRetransmit()); + // 1.2 创建 CoAP 服务器 + coapServer = new CoapServer(config); + + // 2.1 添加 /auth 认证资源 + IotCoapAuthHandler authHandler = new IotCoapAuthHandler(serverId); + IotCoapAuthResource authResource = new IotCoapAuthResource(authHandler); + coapServer.add(authResource); + // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密) + IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler(); + IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler); + // 2.3 添加 /auth/register/sub-device/{productKey}/{deviceName} 子设备动态注册资源 + IotCoapRegisterSubHandler registerSubHandler = new IotCoapRegisterSubHandler(); + IotCoapRegisterSubResource registerSubResource = new IotCoapRegisterSubResource(registerSubHandler); + authResource.add(new CoapResource("register") {{ + add(registerResource); + add(registerSubResource); + }}); + // 2.4 添加 /topic 根资源(用于上行消息) + IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(serverId); + IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(serverId, upstreamHandler); + coapServer.add(topicResource); + + // 3. 启动服务器 + coapServer.start(); + running = true; + log.info("[start][IoT CoAP 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 4. 启动下行消息订阅者 + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e); + if (coapServer != null) { + coapServer.destroy(); + coapServer = null; + } + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + // 1. 停止下行消息订阅者 + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + + // 2. 关闭 CoAP 服务器 + if (coapServer != null) { + try { + coapServer.stop(); + coapServer.destroy(); + coapServer = null; + log.info("[stop][IoT CoAP 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT CoAP 协议 {} 服务器停止失败]", getId(), e); + } + } + running = false; + log.info("[stop][IoT CoAP 协议 {} 已停止]", getId()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java deleted file mode 100644 index 771a33a955..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java +++ /dev/null @@ -1,120 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; - -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthResource; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterResource; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamTopicResource; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamHandler; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.CoapResource; -import org.eclipse.californium.core.CoapServer; -import org.eclipse.californium.core.config.CoapConfig; -import org.eclipse.californium.elements.config.Configuration; - -import java.util.concurrent.TimeUnit; - -/** - * IoT 网关 CoAP 协议:接收设备上行消息 - * - * 基于 Eclipse Californium 实现,支持: - * 1. 认证:POST /auth - * 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post - * 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapUpstreamProtocol implements IotProtocol { - - private static final String ID = "coap"; - - private final IotGatewayProperties.CoapProperties coapProperties; - - private CoapServer coapServer; - - @Getter - private final String serverId; - - private volatile boolean running = false; - - public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) { - this.coapProperties = coapProperties; - this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort()); - } - - @Override - public String getId() { - return ID; - } - - @Override - public IotProtocolTypeEnum getType() { - return IotProtocolTypeEnum.COAP; - } - - @Override - @PostConstruct - public void start() { - try { - // 1.1 创建网络配置(Californium 3.x API) - Configuration config = Configuration.createStandardWithoutFile(); - config.set(CoapConfig.COAP_PORT, coapProperties.getPort()); - config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize()); - config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS); - config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit()); - // 1.2 创建 CoAP 服务器 - coapServer = new CoapServer(config); - - // 2.1 添加 /auth 认证资源 - IotCoapAuthHandler authHandler = new IotCoapAuthHandler(); - IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler); - coapServer.add(authResource); - // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密) - IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler(); - IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler); - authResource.add(new CoapResource("register") {{ - add(registerResource); - }}); - // 2.3 添加 /topic 根资源(用于上行消息) - IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(); - IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler); - coapServer.add(topicResource); - - // 3. 启动服务器 - coapServer.start(); - running = true; - log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 CoAP 协议启动失败]", e); - throw e; - } - } - - @Override - @PreDestroy - public void stop() { - if (coapServer != null) { - try { - coapServer.stop(); - running = false; - log.info("[stop][IoT 网关 CoAP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 CoAP 协议停止失败]", e); - } - } - } - - @Override - public boolean isRunning() { - return running; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java similarity index 76% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java index 8003602d86..188d2e6428 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java @@ -1,8 +1,9 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; import lombok.extern.slf4j.Slf4j; /** @@ -13,7 +14,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber { - public IotCoapDownstreamSubscriber(IotCoapUpstreamProtocol protocol, IotMessageBus messageBus) { + public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) { super(protocol, messageBus); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAbstractHandler.java new file mode 100644 index 0000000000..cda8466dc2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAbstractHandler.java @@ -0,0 +1,186 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.coap.Option; +import org.eclipse.californium.core.server.resources.CoapExchange; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * IoT 网关 CoAP 协议的处理器抽象基类:提供通用的前置处理(认证)、请求解析、响应处理、全局的异常捕获等 + * + * @author 芋道源码 + */ +@Slf4j +public abstract class IotCoapAbstractHandler { + + /** + * 自定义 CoAP Option 编号,用于携带 Token + *

                              + * CoAP Option 范围 2048-65535 属于实验/自定义范围 + */ + public static final int OPTION_TOKEN = 2088; + + private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + + /** + * 处理 CoAP 请求(模板方法) + * + * @param exchange CoAP 交换对象 + */ + public final void handle(CoapExchange exchange) { + try { + // 1. 前置处理 + beforeHandle(exchange); + + // 2. 执行业务逻辑 + CommonResult result = handle0(exchange); + writeResponse(exchange, result); + } catch (ServiceException e) { + // 业务异常,返回对应的错误码和消息 + writeResponse(exchange, CommonResult.error(e.getCode(), e.getMessage())); + } catch (IllegalArgumentException e) { + // 参数校验异常(hutool Assert 抛出),返回 BAD_REQUEST + writeResponse(exchange, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage())); + } catch (Exception e) { + // 其他未知异常,返回 INTERNAL_SERVER_ERROR + log.error("[handle][CoAP 请求处理异常]", e); + writeResponse(exchange, CommonResult.error(INTERNAL_SERVER_ERROR)); + } + } + + /** + * 处理 CoAP 请求(子类实现) + * + * @param exchange CoAP 交换对象 + * @return 处理结果 + */ + protected abstract CommonResult handle0(CoapExchange exchange); + + /** + * 前置处理:认证等 + * + * @param exchange CoAP 交换对象 + */ + private void beforeHandle(CoapExchange exchange) { + // 1.1 如果不需要认证,则不走前置处理 + if (!requiresAuthentication()) { + return; + } + // 1.2 从自定义 Option 获取 token + String token = getTokenFromOption(exchange); + if (StrUtil.isEmpty(token)) { + throw exception(UNAUTHORIZED); + } + // 1.3 校验 token + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); + if (deviceInfo == null) { + throw exception(UNAUTHORIZED); + } + + // 2.1 解析 productKey 和 deviceName + List uriPath = exchange.getRequestOptions().getUriPath(); + String productKey = getProductKey(uriPath); + String deviceName = getDeviceName(uriPath); + if (StrUtil.isEmpty(productKey) || StrUtil.isEmpty(deviceName)) { + throw exception(BAD_REQUEST); + } + // 2.2 校验设备信息是否匹配 + if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) + || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { + throw exception(FORBIDDEN); + } + } + + // ========== Token 相关方法 ========== + + /** + * 是否需要认证(子类可覆盖) + *

                              + * 默认不需要认证 + * + * @return 是否需要认证 + */ + protected boolean requiresAuthentication() { + return false; + } + + /** + * 从 URI 路径中获取 productKey(子类实现) + *

                              + * 默认抛出异常,需要认证的子类必须实现此方法 + * + * @param uriPath URI 路径 + * @return productKey + */ + protected String getProductKey(List uriPath) { + throw new UnsupportedOperationException("子类需要实现 getProductKey 方法"); + } + + /** + * 从 URI 路径中获取 deviceName(子类实现) + *

                              + * 默认抛出异常,需要认证的子类必须实现此方法 + * + * @param uriPath URI 路径 + * @return deviceName + */ + protected String getDeviceName(List uriPath) { + throw new UnsupportedOperationException("子类需要实现 getDeviceName 方法"); + } + + /** + * 从自定义 CoAP Option 中获取 Token + * + * @param exchange CoAP 交换对象 + * @return Token 值,如果不存在则返回 null + */ + protected String getTokenFromOption(CoapExchange exchange) { + Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(), + o -> o.getNumber() == OPTION_TOKEN); + return option != null ? new String(option.getValue()) : null; + } + + // ========== 序列化相关方法 ========== + + /** + * 解析请求体为指定类型 + * + * @param exchange CoAP 交换对象 + * @param clazz 目标类型 + * @param 目标类型泛型 + * @return 解析后的对象,解析失败返回 null + */ + protected T deserializeRequest(CoapExchange exchange, Class clazz) { + byte[] payload = exchange.getRequestPayload(); + if (ArrayUtil.isEmpty(payload)) { + return null; + } + return JsonUtils.parseObject(payload, clazz); + } + + private static String serializeResponse(Object data) { + return JsonUtils.toJsonString(data); + } + + protected void writeResponse(CoapExchange exchange, CommonResult data) { + String json = serializeResponse(data); + exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthHandler.java new file mode 100644 index 0000000000..059d878ade --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthHandler.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.server.resources.CoapExchange; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; + +/** + * IoT 网关 CoAP 协议的【认证】处理器 + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapAuthHandler extends IotCoapAbstractHandler { + + private final String serverId; + + private final IotDeviceTokenService deviceTokenService; + private final IotDeviceCommonApi deviceApi; + private final IotDeviceMessageService deviceMessageService; + + public IotCoapAuthHandler(String serverId) { + this.serverId = serverId; + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + protected CommonResult handle0(CoapExchange exchange) { + // 1. 解析参数 + IotDeviceAuthReqDTO request = deserializeRequest(exchange, IotDeviceAuthReqDTO.class); + Assert.notNull(request, "请求体不能为空"); + Assert.notBlank(request.getClientId(), "clientId 不能为空"); + Assert.notBlank(request.getUsername(), "username 不能为空"); + Assert.notBlank(request.getPassword(), "password 不能为空"); + + // 2.1 执行认证 + CommonResult result = deviceApi.authDevice(request); + result.checkError(); + if (BooleanUtil.isFalse(result.getData())) { + throw exception(DEVICE_AUTH_FAIL); + } + // 2.2 生成 Token + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername()); + Assert.notNull(deviceInfo, "设备信息不能为空"); + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notBlank(token, "生成 token 不能为空"); + + // 3. 执行上线 + IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(message, + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); + + // 4. 构建响应数据 + return CommonResult.success(MapUtil.of("token", token)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthResource.java similarity index 64% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthResource.java index 9d0d90cb3e..0b7a7e6d06 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthResource.java @@ -1,6 +1,5 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.server.resources.CoapExchange; @@ -17,13 +16,10 @@ public class IotCoapAuthResource extends CoapResource { public static final String PATH = "auth"; - private final IotCoapUpstreamProtocol protocol; private final IotCoapAuthHandler authHandler; - public IotCoapAuthResource(IotCoapUpstreamProtocol protocol, - IotCoapAuthHandler authHandler) { + public IotCoapAuthResource(IotCoapAuthHandler authHandler) { super(PATH); - this.protocol = protocol; this.authHandler = authHandler; log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH); } @@ -31,7 +27,7 @@ public class IotCoapAuthResource extends CoapResource { @Override public void handlePOST(CoapExchange exchange) { log.debug("[handlePOST][收到 /auth POST 请求]"); - authHandler.handle(exchange, protocol); + authHandler.handle(exchange); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterHandler.java new file mode 100644 index 0000000000..3dfb6f0df5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterHandler.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.server.resources.CoapExchange; + +/** + * IoT 网关 CoAP 协议的【设备动态注册】处理器 + *

                              + * 用于直连设备/网关的一型一密动态注册,不需要认证 + * + * @author 芋道源码 + * @see 阿里云 - 一型一密 + */ +@Slf4j +public class IotCoapRegisterHandler extends IotCoapAbstractHandler { + + private final IotDeviceCommonApi deviceApi; + + public IotCoapRegisterHandler() { + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + protected CommonResult handle0(CoapExchange exchange) { + // 1. 解析参数 + IotDeviceRegisterReqDTO request = deserializeRequest(exchange, IotDeviceRegisterReqDTO.class); + Assert.notNull(request, "请求体不能为空"); + Assert.notBlank(request.getProductKey(), "productKey 不能为空"); + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); + + // 2. 调用动态注册 + CommonResult result = deviceApi.registerDevice(request); + result.checkError(); + + // 3. 构建响应数据 + return CommonResult.success(result.getData()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterResource.java similarity index 92% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterResource.java index 05fd1ec89d..3a9b5df692 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterResource.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubHandler.java new file mode 100644 index 0000000000..f0f007094e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubHandler.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.server.resources.CoapExchange; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * IoT 网关 CoAP 协议的【子设备动态注册】处理器 + *

                              + * 用于子设备的动态注册,需要网关认证 + * + * @author 芋道源码 + * @see 阿里云 - 动态注册子设备 + */ +@Slf4j +public class IotCoapRegisterSubHandler extends IotCoapAbstractHandler { + + private final IotDeviceCommonApi deviceApi; + + public IotCoapRegisterSubHandler() { + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + protected CommonResult handle0(CoapExchange exchange) { + // 1.1 解析通用参数(从 URI 路径获取网关设备信息) + List uriPath = exchange.getRequestOptions().getUriPath(); + String productKey = getProductKey(uriPath); + String deviceName = getDeviceName(uriPath); + // 1.2 解析子设备列表 + SubDeviceRegisterRequest request = deserializeRequest(exchange, SubDeviceRegisterRequest.class); + Assert.notNull(request, "请求参数不能为空"); + Assert.notEmpty(request.getParams(), "params 不能为空"); + + // 2. 调用子设备动态注册 + IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO() + .setGatewayProductKey(productKey) + .setGatewayDeviceName(deviceName) + .setSubDevices(request.getParams()); + CommonResult> result = deviceApi.registerSubDevices(reqDTO); + result.checkError(); + + // 3. 返回结果 + return success(result.getData()); + } + + @Override + protected boolean requiresAuthentication() { + return true; + } + + @Override + protected String getProductKey(List uriPath) { + // 路径格式:/auth/register/sub-device/{productKey}/{deviceName} + return CollUtil.get(uriPath, 3); + } + + @Override + protected String getDeviceName(List uriPath) { + // 路径格式:/auth/register/sub-device/{productKey}/{deviceName} + return CollUtil.get(uriPath, 4); + } + + @Data + public static class SubDeviceRegisterRequest { + + private List params; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubResource.java new file mode 100644 index 0000000000..1108505360 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubResource.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapResource; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.californium.core.server.resources.Resource; + +/** + * IoT 网关 CoAP 协议的子设备动态注册资源(/auth/register/sub-device/{productKey}/{deviceName}) + *

                              + * 用于子设备的动态注册,需要网关认证 + *

                              + * 支持动态路径匹配:productKey 和 deviceName 是网关设备的标识 + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapRegisterSubResource extends CoapResource { + + public static final String PATH = "sub-device"; + + private final IotCoapRegisterSubHandler registerSubHandler; + + /** + * 创建根资源(/auth/register/sub-device) + */ + public IotCoapRegisterSubResource(IotCoapRegisterSubHandler registerSubHandler) { + this(PATH, registerSubHandler); + log.info("[IotCoapRegisterSubResource][创建 CoAP 子设备动态注册资源: /auth/register/{}]", PATH); + } + + /** + * 创建子资源(动态路径) + */ + private IotCoapRegisterSubResource(String name, IotCoapRegisterSubHandler registerSubHandler) { + super(name); + this.registerSubHandler = registerSubHandler; + } + + @Override + public Resource getChild(String name) { + // 递归创建动态子资源,支持 /sub-device/{productKey}/{deviceName} 路径 + return new IotCoapRegisterSubResource(name, registerSubHandler); + } + + @Override + public void handlePOST(CoapExchange exchange) { + log.debug("[handlePOST][收到子设备动态注册请求]"); + registerSubHandler.handle(exchange); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamHandler.java new file mode 100644 index 0000000000..2d2ee4c6c7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamHandler.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.server.resources.CoapExchange; + +import java.util.List; + +/** + * IoT 网关 CoAP 协议的【上行】处理器 + * + * 处理设备通过 CoAP 协议发送的上行消息,包括: + * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post + * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post + * + * Token 通过自定义 CoAP Option 2088 携带 + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapUpstreamHandler extends IotCoapAbstractHandler { + + private final String serverId; + + private final IotDeviceMessageService deviceMessageService; + + public IotCoapUpstreamHandler(String serverId) { + this.serverId = serverId; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + protected CommonResult handle0(CoapExchange exchange) { + // 1.1 解析通用参数 + List uriPath = exchange.getRequestOptions().getUriPath(); + String productKey = getProductKey(uriPath); + String deviceName = getDeviceName(uriPath); + String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size())); + // 1.2 解析消息 + IotDeviceMessage message = deserializeRequest(exchange, IotDeviceMessage.class); + Assert.notNull(message, "请求参数不能为空"); + Assert.equals(method, message.getMethod(), "method 不匹配"); + + // 2. 发送消息 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); + + // 3. 返回结果 + return CommonResult.success(MapUtil.of("messageId", message.getId())); + } + + @Override + protected boolean requiresAuthentication() { + return true; + } + + @Override + protected String getProductKey(List uriPath) { + // 路径格式:/topic/sys/{productKey}/{deviceName}/... + return CollUtil.get(uriPath, 2); + } + + @Override + protected String getDeviceName(List uriPath) { + // 路径格式:/topic/sys/{productKey}/{deviceName}/... + return CollUtil.get(uriPath, 3); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamTopicResource.java similarity index 70% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamTopicResource.java index 1c694483fa..52d44d3cc6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamTopicResource.java @@ -1,6 +1,5 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.server.resources.CoapExchange; @@ -20,15 +19,15 @@ public class IotCoapUpstreamTopicResource extends CoapResource { public static final String PATH = "topic"; - private final IotCoapUpstreamProtocol protocol; + private final String serverId; private final IotCoapUpstreamHandler upstreamHandler; /** * 创建根资源(/topic) */ - public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol, + public IotCoapUpstreamTopicResource(String serverId, IotCoapUpstreamHandler upstreamHandler) { - this(PATH, protocol, upstreamHandler); + this(PATH, serverId, upstreamHandler); log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH); } @@ -36,32 +35,32 @@ public class IotCoapUpstreamTopicResource extends CoapResource { * 创建子资源(动态路径) */ private IotCoapUpstreamTopicResource(String name, - IotCoapUpstreamProtocol protocol, + String serverId, IotCoapUpstreamHandler upstreamHandler) { super(name); - this.protocol = protocol; + this.serverId = serverId; this.upstreamHandler = upstreamHandler; } @Override public Resource getChild(String name) { // 递归创建动态子资源,支持任意深度路径 - return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler); + return new IotCoapUpstreamTopicResource(name, serverId, upstreamHandler); } @Override public void handleGET(CoapExchange exchange) { - upstreamHandler.handle(exchange, protocol); + upstreamHandler.handle(exchange); } @Override public void handlePOST(CoapExchange exchange) { - upstreamHandler.handle(exchange, protocol); + upstreamHandler.handle(exchange); } @Override public void handlePUT(CoapExchange exchange) { - upstreamHandler.handle(exchange, protocol); + upstreamHandler.handle(exchange); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java index 94536a6439..3de662a5ca 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java @@ -2,12 +2,5 @@ * CoAP 协议实现包 *

                              * 提供基于 Eclipse Californium 的 IoT 设备连接和消息处理功能 - *

                              - * URI 路径: - * - 认证:POST /auth - * - 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post - * - 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post - *

                              - * Token 通过 CoAP Option 2088 携带 */ package cn.iocoder.yudao.module.iot.gateway.protocol.coap; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java deleted file mode 100644 index fa2cc04fdd..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java +++ /dev/null @@ -1,115 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import java.util.Map; - -/** - * IoT 网关 CoAP 协议的【认证】处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapAuthHandler { - - private final IotDeviceTokenService deviceTokenService; - private final IotDeviceCommonApi deviceApi; - private final IotDeviceMessageService deviceMessageService; - - public IotCoapAuthHandler() { - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); - } - - /** - * 处理认证请求 - * - * @param exchange CoAP 交换对象 - * @param protocol 协议对象 - */ - @SuppressWarnings("unchecked") - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) { - try { - // 1.1 解析请求体 - byte[] payload = exchange.getRequestPayload(); - if (payload == null || payload.length == 0) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); - return; - } - Map body; - try { - body = JsonUtils.parseObject(new String(payload), Map.class); - } catch (Exception e) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); - return; - } - // 1.2 解析参数 - String clientId = MapUtil.getStr(body, "clientId"); - if (StrUtil.isEmpty(clientId)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空"); - return; - } - String username = MapUtil.getStr(body, "username"); - if (StrUtil.isEmpty(username)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空"); - return; - } - String password = MapUtil.getStr(body, "password"); - if (StrUtil.isEmpty(password)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空"); - return; - } - - // 2.1 执行认证 - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(clientId).setUsername(username).setPassword(password)); - if (result.isError()) { - log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg()); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg()); - return; - } - if (!BooleanUtil.isTrue(result.getData())) { - log.warn("[handle][认证失败,clientId: {}]", clientId); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败"); - return; - } - // 2.2 生成 Token - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username); - Assert.notNull(deviceInfo, "设备信息不能为空"); - String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - Assert.notBlank(token, "生成 token 不能为空"); - - // 3. 执行上线 - IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(message, - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId()); - - // 4. 返回成功响应 - log.info("[handle][认证成功,productKey: {}, deviceName: {}]", - deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token)); - } catch (Exception e) { - log.error("[handle][认证处理异常]", e); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java deleted file mode 100644 index 08f6cca7a1..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java +++ /dev/null @@ -1,97 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import java.util.Map; - -/** - * IoT 网关 CoAP 协议的【设备动态注册】处理器 - *

                              - * 用于直连设备/网关的一型一密动态注册,不需要认证 - * - * @author 芋道源码 - * @see 阿里云 - 一型一密 - */ -@Slf4j -public class IotCoapRegisterHandler { - - private final IotDeviceCommonApi deviceApi; - - public IotCoapRegisterHandler() { - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - } - - /** - * 处理设备动态注册请求 - * - * @param exchange CoAP 交换对象 - */ - @SuppressWarnings("unchecked") - public void handle(CoapExchange exchange) { - try { - // 1.1 解析请求体 - byte[] payload = exchange.getRequestPayload(); - if (payload == null || payload.length == 0) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); - return; - } - Map body; - try { - body = JsonUtils.parseObject(new String(payload), Map.class); - } catch (Exception e) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); - return; - } - - // 1.2 解析参数 - String productKey = MapUtil.getStr(body, "productKey"); - if (StrUtil.isEmpty(productKey)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空"); - return; - } - String deviceName = MapUtil.getStr(body, "deviceName"); - if (StrUtil.isEmpty(deviceName)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空"); - return; - } - String productSecret = MapUtil.getStr(body, "productSecret"); - if (StrUtil.isEmpty(productSecret)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空"); - return; - } - - // 2. 调用动态注册 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() - .setProductKey(productKey) - .setDeviceName(deviceName) - .setProductSecret(productSecret); - CommonResult result = deviceApi.registerDevice(reqDTO); - if (result.isError()) { - log.warn("[handle][设备动态注册失败,productKey: {}, deviceName: {}, 错误: {}]", - productKey, deviceName, result.getMsg()); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, - "设备动态注册失败:" + result.getMsg()); - return; - } - - // 3. 返回成功响应 - log.info("[handle][设备动态注册成功,productKey: {}, deviceName: {}]", productKey, deviceName); - IotCoapUtils.respondSuccess(exchange, result.getData()); - } catch (Exception e) { - log.error("[handle][设备动态注册处理异常]", e); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java deleted file mode 100644 index d33eb464bb..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java +++ /dev/null @@ -1,110 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.text.StrPool; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ObjUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import java.util.List; - -/** - * IoT 网关 CoAP 协议的【上行】处理器 - * - * 处理设备通过 CoAP 协议发送的上行消息,包括: - * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post - * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post - * - * Token 通过自定义 CoAP Option 2088 携带 - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapUpstreamHandler { - - private final IotDeviceTokenService deviceTokenService; - private final IotDeviceMessageService deviceMessageService; - - public IotCoapUpstreamHandler() { - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); - } - - /** - * 处理 CoAP 请求 - * - * @param exchange CoAP 交换对象 - * @param protocol 协议对象 - */ - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) { - try { - // 1. 解析通用参数 - List uriPath = exchange.getRequestOptions().getUriPath(); - String productKey = CollUtil.get(uriPath, 2); - String deviceName = CollUtil.get(uriPath, 3); - byte[] payload = exchange.getRequestPayload(); - if (StrUtil.isEmpty(productKey)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空"); - return; - } - if (StrUtil.isEmpty(deviceName)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空"); - return; - } - if (ArrayUtil.isEmpty(payload)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); - return; - } - - // 2. 认证:从自定义 Option 获取 token - String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN); - if (StrUtil.isEmpty(token)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空"); - return; - } - // 验证 token - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); - if (deviceInfo == null) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期"); - return; - } - // 验证设备信息匹配 - if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) - || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配"); - return; - } - - // 2.1 解析 method:deviceName 后面的路径,用 . 拼接 - // 路径格式:[topic, sys, productKey, deviceName, thing, property, post] - String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size())); - - // 2.2 解码消息 - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); - if (ObjUtil.notEqual(method, message.getMethod())) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "method 不匹配"); - return; - } - // 2.3 发送消息到消息总线 - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId()); - - // 3. 返回成功响应 - IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId())); - } catch (Exception e) { - log.error("[handle][CoAP 请求处理异常]", e); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java index 9d5cdf3ffb..58e34a2fa8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java @@ -1,14 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util; -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.coap.MediaTypeRegistry; -import org.eclipse.californium.core.coap.Option; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapAbstractHandler; /** * IoT CoAP 协议工具类 @@ -22,63 +14,6 @@ public class IotCoapUtils { *

                              * CoAP Option 范围 2048-65535 属于实验/自定义范围 */ - public static final int OPTION_TOKEN = 2088; - - /** - * 返回成功响应 - * - * @param exchange CoAP 交换对象 - * @param data 响应数据 - */ - public static void respondSuccess(CoapExchange exchange, Object data) { - CommonResult result = CommonResult.success(data); - String json = JsonUtils.toJsonString(result); - exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON); - } - - /** - * 返回错误响应 - * - * @param exchange CoAP 交换对象 - * @param code CoAP 响应码 - * @param message 错误消息 - */ - public static void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) { - int errorCode = mapCoapCodeToErrorCode(code); - CommonResult result = CommonResult.error(errorCode, message); - String json = JsonUtils.toJsonString(result); - exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON); - } - - /** - * 从自定义 CoAP Option 中获取 Token - * - * @param exchange CoAP 交换对象 - * @param optionNumber Option 编号 - * @return Token 值,如果不存在则返回 null - */ - public static String getTokenFromOption(CoapExchange exchange, int optionNumber) { - Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(), - o -> o.getNumber() == optionNumber); - return option != null ? new String(option.getValue()) : null; - } - - /** - * 将 CoAP 响应码映射到业务错误码 - * - * @param code CoAP 响应码 - * @return 业务错误码 - */ - public static int mapCoapCodeToErrorCode(CoAP.ResponseCode code) { - if (code == CoAP.ResponseCode.BAD_REQUEST) { - return BAD_REQUEST.getCode(); - } else if (code == CoAP.ResponseCode.UNAUTHORIZED) { - return UNAUTHORIZED.getCode(); - } else if (code == CoAP.ResponseCode.FORBIDDEN) { - return FORBIDDEN.getCode(); - } else { - return INTERNAL_SERVER_ERROR.getCode(); - } - } + public static final int OPTION_TOKEN = IotCoapAbstractHandler.OPTION_TOKEN; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAbstractHandler.java similarity index 84% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAbstractHandler.java index dbc93a927e..c403ee973f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAbstractHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; @@ -10,9 +10,6 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterHandler; -import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; import io.vertx.core.Handler; import io.vertx.core.http.HttpHeaders; @@ -20,8 +17,7 @@ import io.vertx.ext.web.RoutingContext; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; @@ -35,8 +31,6 @@ public abstract class IotHttpAbstractHandler implements Handler private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - private final IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); - @Override public final void handle(RoutingContext context) { try { @@ -47,15 +41,31 @@ public abstract class IotHttpAbstractHandler implements Handler CommonResult result = handle0(context); writeResponse(context, result); } catch (ServiceException e) { + // 已知异常,返回对应的错误码和错误信息 writeResponse(context, CommonResult.error(e.getCode(), e.getMessage())); + } catch (IllegalArgumentException e) { + // 参数校验异常,返回 400 错误 + writeResponse(context, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage())); } catch (Exception e) { + // 其他未知异常,返回 500 错误 log.error("[handle][path({}) 处理异常]", context.request().path(), e); writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR)); } } + /** + * 处理 HTTP 请求(子类实现) + * + * @param context RoutingContext 对象 + * @return 处理结果 + */ protected abstract CommonResult handle0(RoutingContext context); + /** + * 前置处理:认证等 + * + * @param context RoutingContext 对象 + */ private void beforeHandle(RoutingContext context) { // 如果不需要认证,则不走前置处理 String path = context.request().path(); @@ -102,7 +112,7 @@ public abstract class IotHttpAbstractHandler implements Handler } @SuppressWarnings("deprecation") - public static void writeResponse(RoutingContext context, Object data) { + public static void writeResponse(RoutingContext context, CommonResult data) { context.response() .setStatusCode(200) .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java index 0cc8e35554..21aa5a8fb4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; @@ -11,13 +10,11 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler; import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.ext.web.RoutingContext; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; @@ -48,23 +45,19 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { } @Override + @SuppressWarnings("DuplicatedCode") public CommonResult handle0(RoutingContext context) { // 1. 解析参数 IotDeviceAuthReqDTO request = deserializeRequest(context, IotDeviceAuthReqDTO.class); - if (StrUtil.isEmpty(request.getClientId())) { - throw invalidParamException("clientId 不能为空"); - } - if (StrUtil.isEmpty(request.getUsername())) { - throw invalidParamException("username 不能为空"); - } - if (StrUtil.isEmpty(request.getPassword())) { - throw invalidParamException("password 不能为空"); - } + Assert.notNull(request, "请求参数不能为空"); + Assert.notBlank(request.getClientId(), "clientId 不能为空"); + Assert.notBlank(request.getUsername(), "username 不能为空"); + Assert.notBlank(request.getPassword(), "password 不能为空"); // 2.1 执行认证 CommonResult result = deviceApi.authDevice(request); result.checkError(); - if (!BooleanUtil.isTrue(result.getData())) { + if (BooleanUtil.isFalse(result.getData())) { throw exception(DEVICE_AUTH_FAIL); } // 2.2 生成 Token diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java index ec3bd54b4e..08c60f3c9d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java @@ -1,15 +1,13 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; -import cn.hutool.core.util.StrUtil; +import cn.hutool.core.lang.Assert; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler; import io.vertx.ext.web.RoutingContext; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** @@ -34,15 +32,10 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler { public CommonResult handle0(RoutingContext context) { // 1. 解析参数 IotDeviceRegisterReqDTO request = deserializeRequest(context, IotDeviceRegisterReqDTO.class); - if (StrUtil.isEmpty(request.getProductKey())) { - throw invalidParamException("productKey 不能为空"); - } - if (StrUtil.isEmpty(request.getDeviceName())) { - throw invalidParamException("deviceName 不能为空"); - } - if (StrUtil.isEmpty(request.getProductSecret())) { - throw invalidParamException("productSecret 不能为空"); - } + Assert.notNull(request, "请求参数不能为空"); + Assert.notBlank(request.getProductKey(), "productKey 不能为空"); + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(request); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java index 914de1b795..46932204db 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java @@ -1,19 +1,17 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; -import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler; import io.vertx.ext.web.RoutingContext; import lombok.Data; import java.util.List; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** @@ -46,9 +44,8 @@ public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler { String deviceName = context.pathParam("deviceName"); // 1.2 解析子设备列表 SubDeviceRegisterRequest request = deserializeRequest(context, SubDeviceRegisterRequest.class); - if (CollUtil.isEmpty(request.getParams())) { - throw invalidParamException("params 不能为空"); - } + Assert.notNull(request, "请求参数不能为空"); + Assert.notEmpty(request.getParams(), "params 不能为空"); // 2. 调用子设备动态注册 IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO() diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java index d7f307bb8d..aa408dc79b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java @@ -7,7 +7,6 @@ import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAbstractHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.ext.web.RoutingContext; import lombok.extern.slf4j.Slf4j; @@ -33,15 +32,16 @@ public class IotHttpUpstreamHandler extends IotHttpAbstractHandler { @Override protected CommonResult handle0(RoutingContext context) { - // 1. 解析通用参数 + // 1.1 解析通用参数 String productKey = context.pathParam("productKey"); String deviceName = context.pathParam("deviceName"); String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT); - - // 2.1 根据 Content-Type 反序列化消息 + // 1.2 根据 Content-Type 反序列化消息 IotDeviceMessage message = deserializeRequest(context, IotDeviceMessage.class); + Assert.notNull(message, "请求参数不能为空"); Assert.equals(method, message.getMethod(), "method 不匹配"); - // 2.2 发送消息 + + // 2. 发送消息 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java index fe3a38fe50..45cc3e3ffa 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream; +import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; @@ -26,7 +28,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.util.Assert; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; /** * TCP 上行消息处理器 @@ -132,6 +135,12 @@ public class IotTcpUpstreamHandler implements Handler { // 业务消息 handleBusinessRequest(clientId, message, socket); } + } catch (ServiceException e) { + // 业务异常,返回对应的错误码和错误信息 + log.warn("[processMessage][业务异常,客户端 ID: {},错误: {}]", clientId, e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage()); } catch (IllegalArgumentException e) { // 参数校验失败,返回 400 log.warn("[processMessage][参数校验失败,客户端 ID: {},错误: {}]", clientId, e.getMessage()); @@ -166,10 +175,9 @@ public class IotTcpUpstreamHandler implements Handler { // 2.1 执行认证 CommonResult authResult = deviceApi.authDevice(authParams); - if (authResult.isError()) { - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", clientId, authParams.getUsername()); - sendErrorResponse(socket, message.getRequestId(), AUTH_METHOD, authResult.getCode(), authResult.getMsg()); - return; + authResult.checkError(); + if (BooleanUtil.isFalse(authResult.getData())) { + throw exception(DEVICE_AUTH_FAIL); } // 2.2 解析设备信息 IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); @@ -205,12 +213,7 @@ public class IotTcpUpstreamHandler implements Handler { // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendErrorResponse(socket, message.getRequestId(), IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), - result.getCode(), result.getMsg()); - return; - } + result.checkError(); // 3. 发送成功响应 sendSuccessResponse(socket, message.getRequestId(), diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java index 3e1a4fe143..dd41a52527 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java @@ -1,9 +1,12 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; @@ -27,10 +30,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.util.Assert; import java.net.InetSocketAddress; -import java.time.LocalDateTime; import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; /** * UDP 上行消息处理器 @@ -87,7 +91,7 @@ public class IotUdpUpstreamHandler { this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); } - // TODO @AI:vertx 有 udp 的实现么? + // TODO done @AI:vertx 有 udp 的实现么?当前已使用 Vert.x DatagramSocket 实现 /** * 处理 UDP 数据包 * @@ -99,18 +103,7 @@ public class IotUdpUpstreamHandler { Buffer data = packet.data(); String addressKey = sessionManager.buildAddressKey(senderAddress); log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", addressKey, data.length()); - try { - processMessage(data, senderAddress, socket); - } catch (IllegalArgumentException e) { - // 参数校验失败,返回 400 - log.warn("[handle][参数校验失败,来源: {},错误: {}]", addressKey, e.getMessage()); - sendErrorResponse(socket, senderAddress, null, null, BAD_REQUEST.getCode(), e.getMessage()); - } catch (Exception e) { - // 其他异常,返回 500 - log.error("[handle][处理消息失败,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, null, null, - INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); - } + processMessage(data, senderAddress, socket); } /** @@ -121,6 +114,7 @@ public class IotUdpUpstreamHandler { * @param socket UDP Socket */ private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { + String addressKey = sessionManager.buildAddressKey(senderAddress); // 1.1 基础检查 if (ArrayUtil.isEmpty(buffer)) { return; @@ -133,15 +127,35 @@ public class IotUdpUpstreamHandler { } // 2. 根据消息类型路由处理 - if (AUTH_METHOD.equals(message.getMethod())) { - // 认证请求 - handleAuthenticationRequest(message, senderAddress, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(message, senderAddress, socket); - } else { - // 业务消息 - handleBusinessRequest(message, senderAddress, socket); + try { + if (AUTH_METHOD.equals(message.getMethod())) { + // 认证请求 + handleAuthenticationRequest(message, senderAddress, socket); + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { + // 设备动态注册请求 + handleRegisterRequest(message, senderAddress, socket); + } else { + // 业务消息 + handleBusinessRequest(message, senderAddress, socket); + } + } catch (ServiceException e) { + // 业务异常,返回对应的错误码和错误信息 + log.warn("[processMessage][业务异常,来源: {},requestId: {},method: {},错误: {}]", + addressKey, message.getRequestId(), message.getMethod(), e.getMessage()); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + // 参数校验失败,返回 400 + log.warn("[processMessage][参数校验失败,来源: {},requestId: {},method: {},错误: {}]", + addressKey, message.getRequestId(), message.getMethod(), e.getMessage()); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + BAD_REQUEST.getCode(), e.getMessage()); + } catch (Exception e) { + // 其他异常,返回 500 + log.error("[processMessage][处理消息失败,来源: {},requestId: {},method: {}]", + addressKey, message.getRequestId(), message.getMethod(), e); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); } } @@ -164,12 +178,9 @@ public class IotUdpUpstreamHandler { // 2.1 执行认证 CommonResult authResult = deviceApi.authDevice(authParams); - if (authResult.isError()) { - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", - clientId, authParams.getUsername()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), AUTH_METHOD, - authResult.getCode(), authResult.getMsg()); - return; + authResult.checkError(); + if (!BooleanUtil.isTrue(authResult.getData())) { + throw exception(DEVICE_AUTH_FAIL); } // 2.2 解析设备信息 IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); @@ -187,7 +198,8 @@ public class IotUdpUpstreamHandler { // 4.2 发送上线消息 sendOnlineMessage(device); // 4.3 发送成功响应(包含 token) - sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token); + sendSuccessResponse(socket, senderAddress, message.getRequestId(), AUTH_METHOD, + MapUtil.of("token", token)); log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", device.getId(), device.getDeviceName(), sessionManager.buildAddressKey(senderAddress)); } @@ -211,13 +223,7 @@ public class IotUdpUpstreamHandler { // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", - sessionManager.buildAddressKey(senderAddress), result.getMsg()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getCode(), result.getMsg()); - return; - } + result.checkError(); // 3. 发送成功响应 sendSuccessResponse(socket, senderAddress, message.getRequestId(), @@ -274,17 +280,8 @@ public class IotUdpUpstreamHandler { return; } - // 2. 更新会话活跃时间和地址 - // TODO @AI:是不是合并到 sessionManager 里面更好? - IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(device.getId()); - if (sessionInfo != null) { - // 检查地址是否变化,变化则更新 - if (!senderAddress.equals(sessionInfo.getAddress())) { - sessionManager.updateSessionAddress(device.getId(), senderAddress); - } else { - sessionManager.updateSessionActivity(device.getId()); - } - } + // 2. 更新会话地址(如有变化) + sessionManager.updateSessionAddress(device.getId(), senderAddress); // 3. 将 body 设置为实际的 params,发送消息到消息总线 message.setParams(body); @@ -306,8 +303,7 @@ public class IotUdpUpstreamHandler { .setDeviceId(device.getId()) .setProductKey(device.getProductKey()) .setDeviceName(device.getDeviceName()) - .setAddress(address) - .setLastActiveTime(LocalDateTime.now()); + .setAddress(address); sessionManager.registerSession(device.getId(), sessionInfo); } @@ -324,21 +320,6 @@ public class IotUdpUpstreamHandler { // ===================== 发送响应消息 ===================== - /** - * 发送认证成功响应(包含 token) - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param token JWT Token - */ - private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, String token) { - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, token, - SUCCESS.getCode(), null); - writeResponse(socket, address, responseMessage); - } - /** * 发送成功响应 * diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java index 5280250331..8195c99961 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java @@ -1,25 +1,23 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager; +import cn.hutool.core.util.ObjUtil; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import io.vertx.core.buffer.Buffer; import io.vertx.core.datagram.DatagramSocket; import lombok.Data; import lombok.extern.slf4j.Slf4j; import java.net.InetSocketAddress; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; /** * IoT 网关 UDP 会话管理器 *

                              - * 统一管理 UDP 会话的认证状态、设备会话和消息发送功能: - * 1. 管理 UDP 会话的认证状态 - * 2. 管理设备会话和在线状态 - * 3. 管理消息发送到设备 + * 基于 Guava Cache 实现会话的自动过期清理: + * 1. 管理设备会话信息(设备 ID -> 地址映射) + * 2. 自动清理超时会话(expireAfterAccess) + * 3. 限制最大会话数(maximumSize) * * @author 芋道源码 */ @@ -27,109 +25,76 @@ import java.util.concurrent.ConcurrentHashMap; public class IotUdpSessionManager { /** - * 最大会话数 + * 设备会话缓存:设备 ID -> 会话信息 + *

                              + * 使用 Guava Cache 自动管理过期:expireAfterAccess:每次访问(get/put)自动刷新过期时间 */ + private final Cache deviceSessionCache; + private final int maxSessions; - /** - * 设备 ID -> 会话信息 - */ - private final Map deviceSessionMap = new ConcurrentHashMap<>(); - - /** - * 设备地址 Key -> 设备 ID(反向映射,用于清理时同步) - */ - // TODO @AI:1)这个变量是否必须?2)unregisterSession 这个方法是否必须? - private final Map addressDeviceMap = new ConcurrentHashMap<>(); - - public IotUdpSessionManager(int maxSessions) { + public IotUdpSessionManager(int maxSessions, long sessionTimeoutMs) { this.maxSessions = maxSessions; + this.deviceSessionCache = CacheBuilder.newBuilder() + .maximumSize(maxSessions) + .expireAfterAccess(sessionTimeoutMs, TimeUnit.MILLISECONDS) + .build(); } /** - * 注册设备会话(包含认证信息) + * 注册设备会话 * * @param deviceId 设备 ID * @param sessionInfo 会话信息 */ public void registerSession(Long deviceId, SessionInfo sessionInfo) { - // 检查会话数是否已达上限 - if (deviceSessionMap.size() >= maxSessions) { + // 检查是否为新设备,且会话数已达上限 + if (deviceSessionCache.getIfPresent(deviceId) == null + && deviceSessionCache.size() >= maxSessions) { throw new IllegalStateException("会话数已达上限: " + maxSessions); } - // 如果设备已有其他会话,先清理旧会话 - SessionInfo oldSessionInfo = deviceSessionMap.get(deviceId); - if (oldSessionInfo != null) { - String oldAddressKey = buildAddressKey(oldSessionInfo.getAddress()); - addressDeviceMap.remove(oldAddressKey, deviceId); - log.info("[registerSession][设备已有其他会话,清理旧会话,设备 ID: {},旧地址: {}]", - deviceId, oldAddressKey); - } - - // 注册新会话 - String addressKey = buildAddressKey(sessionInfo.getAddress()); - deviceSessionMap.put(deviceId, sessionInfo); - addressDeviceMap.put(addressKey, deviceId); - log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},product key: {},device name: {}]", - deviceId, addressKey, sessionInfo.getProductKey(), sessionInfo.getDeviceName()); + // 注册会话 + deviceSessionCache.put(deviceId, sessionInfo); + log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},productKey: {},deviceName: {}]", + deviceId, buildAddressKey(sessionInfo.getAddress()), + sessionInfo.getProductKey(), sessionInfo.getDeviceName()); } /** - * 注销设备会话 + * 获取会话信息 + *

                              + * 注意:调用此方法会自动刷新会话的过期时间 * * @param deviceId 设备 ID + * @return 会话信息,不存在则返回 null */ - public void unregisterSession(Long deviceId) { - SessionInfo sessionInfo = deviceSessionMap.remove(deviceId); - if (sessionInfo == null) { - return; - } - String addressKey = buildAddressKey(sessionInfo.getAddress()); - // 仅当 addressDeviceMap 中的 deviceId 是当前 deviceId 时才移除,避免误删新会话 - addressDeviceMap.remove(addressKey, deviceId); - log.info("[unregisterSession][注销设备会话,设备 ID: {},地址: {}]", deviceId, addressKey); - } - - /** - * 更新会话活跃时间(每次收到上行消息时调用) - * - * @param deviceId 设备 ID - */ - public void updateSessionActivity(Long deviceId) { - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); - if (sessionInfo != null) { - sessionInfo.setLastActiveTime(LocalDateTime.now()); - } + public SessionInfo getSession(Long deviceId) { + return deviceSessionCache.getIfPresent(deviceId); } /** * 更新设备会话地址(设备地址变更时调用) + *

                              + * 注意:getIfPresent 已自动刷新过期时间,无需重新 put * * @param deviceId 设备 ID * @param newAddress 新地址 */ public void updateSessionAddress(Long deviceId, InetSocketAddress newAddress) { - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); + // 地址未变化,无需更新 + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId); if (sessionInfo == null) { return; } - // 清理旧地址映射 + if (ObjUtil.equals(newAddress, sessionInfo.getAddress())) { + return; + } + + // 更新地址 String oldAddressKey = buildAddressKey(sessionInfo.getAddress()); - addressDeviceMap.remove(oldAddressKey, deviceId); - - // 更新新地址 - String newAddressKey = buildAddressKey(newAddress); sessionInfo.setAddress(newAddress); - sessionInfo.setLastActiveTime(LocalDateTime.now()); - addressDeviceMap.put(newAddressKey, deviceId); - log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},新地址: {}]", deviceId, newAddressKey); - } - - /** - * 获取会话信息 - */ - public SessionInfo getSessionInfo(Long deviceId) { - return deviceSessionMap.get(deviceId); + log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},旧地址: {},新地址: {}]", + deviceId, oldAddressKey, buildAddressKey(newAddress)); } /** @@ -141,7 +106,7 @@ public class IotUdpSessionManager { * @return 是否发送成功 */ public boolean sendToDevice(Long deviceId, byte[] data, DatagramSocket socket) { - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId); if (sessionInfo == null || sessionInfo.getAddress() == null) { log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId); return false; @@ -165,38 +130,7 @@ public class IotUdpSessionManager { } /** - * 定期清理不活跃的设备会话 - * - * @param timeoutMs 超时时间(毫秒) - * @return 清理的设备 ID 列表(用于发送离线消息) - */ - public List cleanExpiredSessions(long timeoutMs) { - List offlineDeviceIds = new ArrayList<>(); - LocalDateTime now = LocalDateTime.now(); - LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000); - Iterator> iterator = deviceSessionMap.entrySet().iterator(); - // TODO @AI:改成 for each 会不会更好? - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - SessionInfo sessionInfo = entry.getValue(); - // 未过期,跳过 - if (sessionInfo.getLastActiveTime().isAfter(expireTime)) { - continue; - } - // 过期处理:记录离线设备 ID - Long deviceId = entry.getKey(); - String addressKey = buildAddressKey(sessionInfo.getAddress()); - addressDeviceMap.remove(addressKey, deviceId); - offlineDeviceIds.add(deviceId); - log.debug("[cleanExpiredSessions][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]", - deviceId, addressKey, sessionInfo.getLastActiveTime()); - iterator.remove(); - } - return offlineDeviceIds; - } - - /** - * 构建地址 Key + * 构建地址 Key(用于日志输出) * * @param address 地址 * @return 地址 Key @@ -206,7 +140,7 @@ public class IotUdpSessionManager { } /** - * 会话信息(包含认证信息) + * 会话信息 */ @Data public static class SessionInfo { @@ -228,10 +162,6 @@ public class IotUdpSessionManager { * 设备地址 */ private InetSocketAddress address; - /** - * 最后活跃时间 - */ - private LocalDateTime lastActiveTime; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 7691fc9b4a..ddc353f399 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -81,13 +81,38 @@ yudao: - id: udp-json type: udp port: 8093 - enabled: true + enabled: false serialize: json udp: max-sessions: 1000 # 最大会话数 session-timeout-ms: 60000 # 会话超时时间(毫秒),基于 Guava Cache 自动过期 receive-buffer-size: 65536 # 接收缓冲区大小(字节) send-buffer-size: 65536 # 发送缓冲区大小(字节) + # ==================================== + # 针对引入的 WebSocket 组件的配置 + # ==================================== + - id: websocket-json + type: websocket + port: 8094 + enabled: false + serialize: json + websocket: + path: /ws + max-message-size: 65536 # 最大消息大小(字节,默认 64KB) + max-frame-size: 65536 # 最大帧大小(字节,默认 64KB) + idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60) + ssl-enabled: false # 是否启用 SSL(wss://) + # ==================================== + # 针对引入的 CoAP 组件的配置 + # ==================================== + - id: coap-json + type: coap + port: 5683 + enabled: true + coap: + max-message-size: 1024 # 最大消息大小(字节) + ack-timeout-ms: 2000 # ACK 超时时间(毫秒) + max-retransmit: 4 # 最大重传次数 # 协议配置(旧版,保持兼容) protocol: @@ -134,27 +159,6 @@ yudao: max-message-size: 8192 connect-timeout-seconds: 60 ssl-enabled: false - # ==================================== - # 针对引入的 CoAP 组件的配置 - # ==================================== - coap: - enabled: false # 是否启用 CoAP 协议 - port: 5683 # CoAP 服务端口(默认 5683) - max-message-size: 1024 # 最大消息大小(字节) - ack-timeout: 2000 # ACK 超时时间(毫秒) - max-retransmit: 4 # 最大重传次数 - # ==================================== - # 针对引入的 WebSocket 组件的配置 - # ==================================== - websocket: - enabled: false # 是否启用 WebSocket 协议 - port: 8094 # WebSocket 服务端口(默认 8094) - path: /ws # WebSocket 路径(默认 /ws) - max-message-size: 65536 # 最大消息大小(字节,默认 64KB) - max-frame-size: 65536 # 最大帧大小(字节,默认 64KB) - idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60) - ssl-enabled: false # 是否启用 SSL(wss://) - --- #################### 日志相关配置 #################### # 基础日志配置 From 3db187091c7e510b651b4e6452a866008a02b4b4 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 17:09:14 +0800 Subject: [PATCH 16/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20coap=20=E5=8D=95=E6=B5=8B=E7=B1=BB=E7=9A=84?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../protocol/coap/IotCoapProtocol.java | 16 ++++++++-------- .../IotCoapAbstractHandler.java | 2 +- .../IotCoapAuthHandler.java | 2 +- .../IotCoapAuthResource.java | 2 +- .../IotCoapRegisterHandler.java | 2 +- .../IotCoapRegisterResource.java | 2 +- .../IotCoapRegisterSubHandler.java | 2 +- .../IotCoapRegisterSubResource.java | 2 +- .../IotCoapUpstreamHandler.java | 2 +- .../IotCoapUpstreamTopicResource.java | 2 +- .../protocol/coap/util/IotCoapUtils.java | 19 ------------------- ...rectDeviceCoapProtocolIntegrationTest.java | 7 ++++--- ...ewayDeviceCoapProtocolIntegrationTest.java | 13 +++++++------ ...ySubDeviceCoapProtocolIntegrationTest.java | 7 ++++--- 14 files changed, 32 insertions(+), 48 deletions(-) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/{upstrem => upstream}/IotCoapAbstractHandler.java (99%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/{upstrem => upstream}/IotCoapAuthHandler.java (99%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/{upstrem => upstream}/IotCoapAuthResource.java (98%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/{upstrem => upstream}/IotCoapRegisterHandler.java (99%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/{upstrem => upstream}/IotCoapRegisterResource.java (98%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/{upstrem => upstream}/IotCoapRegisterSubHandler.java (99%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/{upstrem => upstream}/IotCoapRegisterSubResource.java (99%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/{upstrem => upstream}/IotCoapUpstreamHandler.java (99%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/{upstrem => upstream}/IotCoapUpstreamTopicResource.java (99%) delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java index 2749bd232c..28fa998807 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java @@ -7,14 +7,14 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapAuthHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapAuthResource; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterResource; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterSubHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapRegisterSubResource; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapUpstreamTopicResource; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthResource; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterResource; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterSubHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterSubResource; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapUpstreamTopicResource; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAbstractHandler.java similarity index 99% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAbstractHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAbstractHandler.java index cda8466dc2..994fb147d2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAbstractHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAbstractHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthHandler.java similarity index 99% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthHandler.java index 059d878ade..0b1914e091 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthResource.java similarity index 98% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthResource.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthResource.java index 0b7a7e6d06..95b6fefd46 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapAuthResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthResource.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java similarity index 99% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java index 3dfb6f0df5..a00cce4971 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.extra.spring.SpringUtil; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterResource.java similarity index 98% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterResource.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterResource.java index 3a9b5df692..f8f6b0cf9a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterResource.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubHandler.java similarity index 99% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubHandler.java index f0f007094e..8827cc3dbb 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubResource.java similarity index 99% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubResource.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubResource.java index 1108505360..3cc42b606a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapRegisterSubResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubResource.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamHandler.java similarity index 99% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamHandler.java index 2d2ee4c6c7..d9e349ba58 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamTopicResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamTopicResource.java similarity index 99% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamTopicResource.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamTopicResource.java index 52d44d3cc6..65185b575d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstrem/IotCoapUpstreamTopicResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamTopicResource.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java deleted file mode 100644 index 58e34a2fa8..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util; - -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstrem.IotCoapAbstractHandler; - -/** - * IoT CoAP 协议工具类 - * - * @author 芋道源码 - */ -public class IotCoapUtils { - - /** - * 自定义 CoAP Option 编号,用于携带 Token - *

                              - * CoAP Option 范围 2048-65535 属于实验/自定义范围 - */ - public static final int OPTION_TOKEN = IotCoapAbstractHandler.OPTION_TOKEN; - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java index 583763e22c..6c852affca 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java @@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapResponse; @@ -23,6 +22,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAbstractHandler.OPTION_TOKEN; + /** * IoT 直连设备 CoAP 协议集成测试(手动测试) * @@ -134,7 +135,7 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -177,7 +178,7 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java index ca581cb960..f350325dd8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java @@ -13,7 +13,6 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapResponse; @@ -30,6 +29,8 @@ import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.Map; +import static cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAbstractHandler.OPTION_TOKEN; + /** * IoT 网关设备 CoAP 协议集成测试(手动测试) * @@ -158,7 +159,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -201,7 +202,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -242,7 +243,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -289,7 +290,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -362,7 +363,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java index 7aed8ecb65..4d909a2d29 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java @@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapResponse; @@ -22,6 +21,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAbstractHandler.OPTION_TOKEN; + /** * IoT 网关子设备 CoAP 协议集成测试(手动测试) * @@ -137,7 +138,7 @@ public class IotGatewaySubDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -185,7 +186,7 @@ public class IotGatewaySubDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 From d9a08094d9f9640f94f7b2e3cb94961019b8efef Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 18:43:50 +0800 Subject: [PATCH 17/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91websocket?= =?UTF-8?q?=20=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=8850%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gateway/protocol/IotProtocolManager.java | 6 +- .../gateway/protocol/tcp/IotTcpProtocol.java | 10 +- .../length/IotTcpLengthFieldFrameCodec.java | 6 + .../upstream/IotTcpUpstreamHandler.java | 28 +-- .../gateway/protocol/udp/IotUdpProtocol.java | 9 +- .../websocket/IotWebSocketConfig.java | 54 +++++ .../websocket/IotWebSocketProtocol.java | 218 ++++++++++++++++++ .../IotWebSocketUpstreamProtocol.java | 135 ----------- .../IotWebSocketDownstreamHandler.java | 21 +- .../IotWebSocketDownstreamSubscriber.java | 6 +- .../IotWebSocketUpstreamHandler.java | 142 ++++++++---- .../IotWebSocketConnectionManager.java | 42 +++- .../src/main/resources/application.yaml | 3 +- ...irectDeviceTcpProtocolIntegrationTest.java | 2 +- ...tewayDeviceTcpProtocolIntegrationTest.java | 2 +- ...aySubDeviceTcpProtocolIntegrationTest.java | 2 +- ...eviceWebSocketProtocolIntegrationTest.java | 42 ++-- ...eviceWebSocketProtocolIntegrationTest.java | 60 ++--- ...eviceWebSocketProtocolIntegrationTest.java | 36 +-- 19 files changed, 522 insertions(+), 302 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/{router => handler/downstream}/IotWebSocketDownstreamHandler.java (70%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/{ => handler/downstream}/IotWebSocketDownstreamSubscriber.java (78%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/{router => handler/upstream}/IotWebSocketUpstreamHandler.java (80%) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index ed60897e55..45b6789041 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.context.SmartLifecycle; @@ -158,8 +158,8 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return WebSocket 协议实例 */ - private IotWebSocketUpstreamProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { - return new IotWebSocketUpstreamProtocol(config); + private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + return new IotWebSocketProtocol(config); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java index 937745c584..3a31f505b5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java @@ -57,6 +57,10 @@ public class IotTcpProtocol implements IotProtocol { * TCP 服务器 */ private NetServer tcpServer; + /** + * TCP 连接管理器 + */ + private final IotTcpConnectionManager connectionManager; /** * 下行消息订阅者 @@ -67,17 +71,11 @@ public class IotTcpProtocol implements IotProtocol { * 消息序列化器 */ private final IotMessageSerializer serializer; - /** * TCP 帧编解码器 */ private final IotTcpFrameCodec frameCodec; - /** - * TCP 连接管理器 - */ - private final IotTcpConnectionManager connectionManager; - public IotTcpProtocol(ProtocolInstanceProperties properties) { IotTcpConfig tcpConfig = properties.getTcp(); Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java index 0fbe42d7c9..4200b6b1fb 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java @@ -29,6 +29,11 @@ import java.util.concurrent.atomic.AtomicReference; @Slf4j public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec { + /** + * 最大帧长度(64KB),防止 DoS 攻击 + */ + private static final int MAX_FRAME_LENGTH = 65536; + private final int lengthFieldOffset; private final int lengthFieldLength; private final int lengthAdjustment; @@ -60,6 +65,7 @@ public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec { public RecordParser createDecodeParser(Handler handler) { // 创建状态机:先读取头部,再读取消息体 RecordParser parser = RecordParser.newFixed(headerLength); + parser.maxRecordSize(MAX_FRAME_LENGTH); // 设置最大记录大小,防止 DoS 攻击 final AtomicReference bodyLength = new AtomicReference<>(null); // 消息体长度,null 表示读取头部阶段 final AtomicReference headerBuffer = new AtomicReference<>(null); // 头部消息 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java index 45cc3e3ffa..9542b504c2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java @@ -77,7 +77,9 @@ public class IotTcpUpstreamHandler implements Handler { } @Override + @SuppressWarnings("DuplicatedCode") public void handle(NetSocket socket) { + // TODO @AI:clientId 去掉;其它模块也看看,怎么去掉下看看; String clientId = IdUtil.simpleUUID(); log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); @@ -92,9 +94,10 @@ public class IotTcpUpstreamHandler implements Handler { }); // 2.1 设置消息处理器 + // TODO @AI:去掉 clientId; Handler messageHandler = buffer -> { try { - processMessage(clientId, buffer, socket); + processMessage(buffer, socket); } catch (Exception e) { log.error("[handle][消息处理失败,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress(), e); @@ -110,11 +113,10 @@ public class IotTcpUpstreamHandler implements Handler { /** * 处理消息 * - * @param clientId 客户端 ID * @param buffer 消息 * @param socket 网络连接 */ - private void processMessage(String clientId, Buffer buffer, NetSocket socket) { + private void processMessage(Buffer buffer, NetSocket socket) { IotDeviceMessage message = null; try { // 1. 反序列化消息 @@ -127,29 +129,29 @@ public class IotTcpUpstreamHandler implements Handler { // 2. 根据消息类型路由处理 if (AUTH_METHOD.equals(message.getMethod())) { // 认证请求 - handleAuthenticationRequest(clientId, message, socket); + handleAuthenticationRequest(message, socket); } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { // 设备动态注册请求 - handleRegisterRequest(clientId, message, socket); + handleRegisterRequest(message, socket); } else { // 业务消息 - handleBusinessRequest(clientId, message, socket); + handleBusinessRequest(null, message, socket); } } catch (ServiceException e) { // 业务异常,返回对应的错误码和错误信息 - log.warn("[processMessage][业务异常,客户端 ID: {},错误: {}]", clientId, e.getMessage()); + log.warn("[processMessage][业务异常,客户端 ID: {},错误: {}]", null, e.getMessage()); String requestId = message != null ? message.getRequestId() : null; String method = message != null ? message.getMethod() : null; sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage()); } catch (IllegalArgumentException e) { // 参数校验失败,返回 400 - log.warn("[processMessage][参数校验失败,客户端 ID: {},错误: {}]", clientId, e.getMessage()); + log.warn("[processMessage][参数校验失败,客户端 ID: {},错误: {}]", null, e.getMessage()); String requestId = message != null ? message.getRequestId() : null; String method = message != null ? message.getMethod() : null; sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage()); } catch (Exception e) { // 其他异常,返回 500,并重新抛出让上层关闭连接 - log.error("[processMessage][处理消息失败,客户端 ID: {}]", clientId, e); + log.error("[processMessage][处理消息失败,客户端 ID: {}]", null, e); String requestId = message != null ? message.getRequestId() : null; String method = message != null ? message.getMethod() : null; sendErrorResponse(socket, requestId, method, @@ -161,12 +163,11 @@ public class IotTcpUpstreamHandler implements Handler { /** * 处理认证请求 * - * @param clientId 客户端 ID * @param message 消息信息 * @param socket 网络连接 */ @SuppressWarnings("DuplicatedCode") - private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, NetSocket socket) { + private void handleAuthenticationRequest(IotDeviceMessage message, NetSocket socket) { // 1. 解析认证参数 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); Assert.notNull(authParams, "认证参数不能为空"); @@ -198,13 +199,12 @@ public class IotTcpUpstreamHandler implements Handler { /** * 处理设备动态注册请求(一型一密,不需要认证) * - * @param clientId 客户端 ID * @param message 消息信息 * @param socket 网络连接 * @see 阿里云 - 一型一密 */ @SuppressWarnings("DuplicatedCode") - private void handleRegisterRequest(String clientId, IotDeviceMessage message, NetSocket socket) { + private void handleRegisterRequest(IotDeviceMessage message, NetSocket socket) { // 1. 解析注册参数 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); Assert.notNull(params, "注册参数不能为空"); @@ -218,7 +218,7 @@ public class IotTcpUpstreamHandler implements Handler { // 3. 发送成功响应 sendSuccessResponse(socket, message.getRequestId(), IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); - log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", clientId, params.getDeviceName()); + log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", null, params.getDeviceName()); } /** diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java index 96f523dfd8..647a713b55 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java @@ -55,6 +55,10 @@ public class IotUdpProtocol implements IotProtocol { */ @Getter private DatagramSocket udpSocket; + /** + * UDP 会话管理器 + */ + private final IotUdpSessionManager sessionManager; /** * 下行消息订阅者 @@ -66,11 +70,6 @@ public class IotUdpProtocol implements IotProtocol { */ private final IotMessageSerializer serializer; - /** - * UDP 会话管理器 - */ - private final IotUdpSessionManager sessionManager; - public IotUdpProtocol(ProtocolInstanceProperties properties) { IotUdpConfig udpConfig = properties.getUdp(); Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java new file mode 100644 index 0000000000..e64e11dc51 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT WebSocket 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotWebSocketConfig { + + /** + * WebSocket 路径(默认:/ws) + */ + @NotEmpty(message = "WebSocket 路径不能为空") + private String path = "/ws"; + + /** + * 最大消息大小(字节,默认 64KB) + */ + @NotNull(message = "最大消息大小不能为空") + private Integer maxMessageSize = 65536; + /** + * 最大帧大小(字节,默认 64KB) + */ + @NotNull(message = "最大帧大小不能为空") + private Integer maxFrameSize = 65536; + + /** + * 空闲超时时间(秒,默认 60) + */ + @NotNull(message = "空闲超时时间不能为空") + private Integer idleTimeoutSeconds = 60; + + /** + * 是否启用 SSL(wss://) + */ + @NotNull(message = "是否启用 SSL 不能为空") + private Boolean sslEnabled = false; + + /** + * SSL 证书路径 + */ + private String sslCertPath; + + /** + * SSL 私钥路径 + */ + private String sslKeyPath; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java new file mode 100644 index 0000000000..112c5acf5c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java @@ -0,0 +1,218 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; + +import cn.hutool.core.util.ObjUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.upstream.IotWebSocketUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.PemKeyCertOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; + +// TODO @AI:注释调整下,参考 IotTcpProtocol +/** + * IoT 网关 WebSocket 协议:接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotWebSocketProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolInstanceProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * WebSocket 服务器 + */ + private HttpServer httpServer; + /** + * WebSocket 连接管理器 + */ + private final IotWebSocketConnectionManager connectionManager; + + // TODO @AI:可以不用这个变量,从 properties 里面获取 + /** + * WebSocket 配置 + */ + private final IotWebSocketConfig wsConfig; + + /** + * 下行消息订阅者 + */ + private final IotWebSocketDownstreamSubscriber downstreamSubscriber; + + /** + * 消息序列化器 + */ + private final IotMessageSerializer serializer; + + private final IotDeviceService deviceService; + private final IotDeviceMessageService messageService; + + public IotWebSocketProtocol(ProtocolInstanceProperties properties) { + Assert.notNull(properties, "协议实例配置不能为空"); + Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空"); + this.properties = properties; + this.wsConfig = properties.getWebsocket(); + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); + this.serializer = serializerManager.get(serializeType); + + // 初始化基础依赖 + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.messageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.connectionManager = new IotWebSocketConnectionManager(); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotWebSocketDownstreamHandler downstreamHandler = new IotWebSocketDownstreamHandler(serializer, connectionManager); + this.downstreamSubscriber = new IotWebSocketDownstreamSubscriber(this, downstreamHandler, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.WEBSOCKET; + } + + @Override + @SuppressWarnings("deprecation") + public void start() { + if (running) { + log.warn("[start][IoT WebSocket 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + + // 1.2 创建服务器选项 + HttpServerOptions options = new HttpServerOptions() + .setPort(properties.getPort()) + .setIdleTimeout(wsConfig.getIdleTimeoutSeconds()) + .setMaxWebSocketFrameSize(wsConfig.getMaxFrameSize()) + .setMaxWebSocketMessageSize(wsConfig.getMaxMessageSize()); + if (Boolean.TRUE.equals(wsConfig.getSslEnabled())) { + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(wsConfig.getSslKeyPath()) + .setCertPath(wsConfig.getSslCertPath()); + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + + // 1.3 创建服务器并设置 WebSocket 处理器 + httpServer = vertx.createHttpServer(options); + httpServer.webSocketHandler(socket -> { + // 验证路径 + if (ObjUtil.notEqual(wsConfig.getPath(), socket.path())) { + log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]", + socket.path(), wsConfig.getPath()); + socket.reject(); + return; + } + // 创建上行处理器 + IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(this, + messageService, deviceService, connectionManager, serializer); + handler.handle(socket); + }); + + // 1.4 启动服务器 + try { + httpServer.listen().result(); + running = true; + log.info("[start][IoT WebSocket 协议 {} 启动成功,端口:{},路径:{},serverId:{}]", + getId(), properties.getPort(), wsConfig.getPath(), serverId); + + // 2. 启动下行消息订阅者 + downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT WebSocket 协议 {} 启动失败]", getId(), e); + if (httpServer != null) { + httpServer.close(); + httpServer = null; + } + if (vertx != null) { + vertx.close(); + vertx = null; + } + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + // 1. 停止下行消息订阅者 + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT WebSocket 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT WebSocket 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + + // 2.1 关闭 WebSocket 服务器 + if (httpServer != null) { + try { + httpServer.close().result(); + log.info("[stop][IoT WebSocket 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT WebSocket 协议 {} 服务器停止失败]", getId(), e); + } + httpServer = null; + } + // 2.2 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT WebSocket 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT WebSocket 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT WebSocket 协议 {} 已停止]", getId()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java deleted file mode 100644 index 75465954da..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java +++ /dev/null @@ -1,135 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; - -import cn.hutool.core.util.ObjUtil; -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.net.PemKeyCertOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 WebSocket 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotWebSocketUpstreamProtocol implements IotProtocol { - - private static final String ID = "websocket"; - - private final IotGatewayProperties.WebSocketProperties wsProperties; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotWebSocketConnectionManager connectionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private HttpServer httpServer; - - private volatile boolean running = false; - - public IotWebSocketUpstreamProtocol(IotGatewayProperties.WebSocketProperties wsProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotWebSocketConnectionManager connectionManager, - Vertx vertx) { - this.wsProperties = wsProperties; - this.deviceService = deviceService; - this.messageService = messageService; - this.connectionManager = connectionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(wsProperties.getPort()); - } - - @Override - public String getId() { - return ID; - } - - @Override - public IotProtocolTypeEnum getType() { - return IotProtocolTypeEnum.WEBSOCKET; - } - - @Override - @PostConstruct - @SuppressWarnings("deprecation") - public void start() { - // 1.1 创建服务器选项 - HttpServerOptions options = new HttpServerOptions() - .setPort(wsProperties.getPort()) - .setIdleTimeout(wsProperties.getIdleTimeoutSeconds()) - .setMaxWebSocketFrameSize(wsProperties.getMaxFrameSize()) - .setMaxWebSocketMessageSize(wsProperties.getMaxMessageSize()); - // 1.2 配置 SSL(如果启用) - if (Boolean.TRUE.equals(wsProperties.getSslEnabled())) { - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(wsProperties.getSslKeyPath()) - .setCertPath(wsProperties.getSslCertPath()); - options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); - } - - // 2. 创建服务器并设置 WebSocket 处理器 - httpServer = vertx.createHttpServer(options); - httpServer.webSocketHandler(socket -> { - // 验证路径 - if (ObjUtil.notEqual(wsProperties.getPath(), socket.path())) { - log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]", - socket.path(), wsProperties.getPath()); - socket.reject(); - return; - } - // 创建上行处理器 - IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(this, - messageService, deviceService, connectionManager); - handler.handle(socket); - }); - - // 3. 启动服务器 - try { - httpServer.listen().result(); - running = true; - log.info("[start][IoT 网关 WebSocket 协议启动成功,端口:{},路径:{}]", wsProperties.getPort(), wsProperties.getPath()); - } catch (Exception e) { - log.error("[start][IoT 网关 WebSocket 协议启动失败]", e); - throw e; - } - } - - @Override - @PreDestroy - public void stop() { - if (httpServer != null) { - try { - httpServer.close().result(); - running = false; - log.info("[stop][IoT 网关 WebSocket 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 WebSocket 协议停止失败]", e); - } - } - } - - @Override - public boolean isRunning() { - return running; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamHandler.java similarity index 70% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamHandler.java index 05e3c8c91f..096435eacb 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamHandler.java @@ -1,9 +1,10 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -16,7 +17,7 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public class IotWebSocketDownstreamHandler { - private final IotDeviceMessageService deviceMessageService; + private final IotMessageSerializer serializer; private final IotWebSocketConnectionManager connectionManager; @@ -37,9 +38,15 @@ public class IotWebSocketDownstreamHandler { } // 2. 编码消息并发送到设备 - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType()); - String jsonMessage = StrUtil.utf8Str(bytes); - boolean success = connectionManager.sendToDevice(message.getDeviceId(), jsonMessage); + byte[] bytes = serializer.serialize(message); + // TODO @AI:参考别的模块的做法,直接发?类似 tcp 这种; + boolean success; + if (serializer.getType() == IotSerializeTypeEnum.BINARY) { + success = connectionManager.sendToDevice(message.getDeviceId(), bytes); + } else { + String jsonMessage = StrUtil.utf8Str(bytes); + success = connectionManager.sendToDevice(message.getDeviceId(), jsonMessage); + } if (success) { log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java similarity index 78% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java index 4b11bb02be..efe5f437e8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java @@ -1,9 +1,9 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; import lombok.extern.slf4j.Slf4j; /** @@ -16,7 +16,7 @@ public class IotWebSocketDownstreamSubscriber extends IotProtocolDownstreamSubsc private final IotWebSocketDownstreamHandler downstreamHandler; - public IotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocol, + public IotWebSocketDownstreamSubscriber(IotWebSocketProtocol protocol, IotWebSocketDownstreamHandler downstreamHandler, IotMessageBus messageBus) { super(protocol, messageBus); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java similarity index 80% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java index 630246afa3..2b6b5e4fbe 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java @@ -1,6 +1,7 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.upstream; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; @@ -11,17 +12,19 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; import io.vertx.core.http.ServerWebSocket; import lombok.extern.slf4j.Slf4j; @@ -36,82 +39,120 @@ import java.util.Map; @Slf4j public class IotWebSocketUpstreamHandler implements Handler { - /** - * 默认消息编解码类型 - */ - private static final String CODEC_TYPE = IotAlinkDeviceMessageCodec.TYPE; - private static final String AUTH_METHOD = "auth"; - private final IotDeviceMessageService deviceMessageService; - - private final IotDeviceService deviceService; - - private final IotWebSocketConnectionManager connectionManager; - - private final IotDeviceCommonApi deviceApi; - private final String serverId; - public IotWebSocketUpstreamHandler(IotWebSocketUpstreamProtocol protocol, + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + /** + * 连接管理器 + */ + private final IotWebSocketConnectionManager connectionManager; + + // TODO @AI:是不是可以去掉? + private final boolean binaryPayload; + + private final IotDeviceMessageService deviceMessageService; + private final IotDeviceService deviceService; + private final IotDeviceCommonApi deviceApi; + + // TODO @AI:参数、顺序参考 IotTcpUpstreamHandler + public IotWebSocketUpstreamHandler(IotWebSocketProtocol protocol, IotDeviceMessageService deviceMessageService, IotDeviceService deviceService, - IotWebSocketConnectionManager connectionManager) { + IotWebSocketConnectionManager connectionManager, + IotMessageSerializer serializer) { this.deviceMessageService = deviceMessageService; this.deviceService = deviceService; this.connectionManager = connectionManager; + this.serializer = serializer; + this.binaryPayload = serializer.getType() == IotSerializeTypeEnum.BINARY; this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); this.serverId = protocol.getServerId(); + // TODO @AI:通过 springutil;deviceService、deviceMessageService; } @Override + @SuppressWarnings("DuplicatedCode") public void handle(ServerWebSocket socket) { String clientId = IdUtil.simpleUUID(); log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); // 1. 设置异常和关闭处理器 + // TODO @AI:clientId 去掉; socket.exceptionHandler(ex -> { log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - cleanupConnection(socket); + socket.close(); }); socket.closeHandler(v -> { log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); cleanupConnection(socket); }); - // 2. 设置文本消息处理器 - socket.textMessageHandler(message -> { - try { - processMessage(clientId, message, socket); - } catch (Exception e) { - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", - clientId, socket.remoteAddress(), e.getMessage()); + // 2. 设置消息处理器(JSON 使用文本,BINARY 使用二进制) + // TODO @AI:是不是 text、binary 保持统一?用一个 mesagehandler? + if (binaryPayload) { + socket.binaryMessageHandler(buffer -> { + try { + processMessage(clientId, buffer.getBytes(), socket); + } catch (Exception e) { + log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", + clientId, socket.remoteAddress(), e.getMessage()); + cleanupConnection(socket); + socket.close(); + } + }); + socket.textMessageHandler(message -> { + log.warn("[handle][收到文本帧但当前序列化为 BINARY,断开连接,客户端 ID: {},地址: {}]", + clientId, socket.remoteAddress()); cleanupConnection(socket); socket.close(); - } - }); + }); + } else { + socket.textMessageHandler(message -> { + try { + processMessage(clientId, StrUtil.utf8Bytes(message), socket); + } catch (Exception e) { + log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", + clientId, socket.remoteAddress(), e.getMessage()); + // TODO @AI:是不是不用 cleanupConnection?closehandler 本身就吹了了; + cleanupConnection(socket); + socket.close(); + } + }); + socket.binaryMessageHandler(buffer -> { + try { + processMessage(clientId, buffer.getBytes(), socket); + } catch (Exception e) { + log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", + clientId, socket.remoteAddress(), e.getMessage()); + cleanupConnection(socket); + socket.close(); + } + }); + } } /** * 处理消息 * * @param clientId 客户端 ID - * @param message 消息(JSON 字符串) + * @param payload 消息负载 * @param socket WebSocket 连接 * @throws Exception 消息解码失败时抛出异常 */ - private void processMessage(String clientId, String message, ServerWebSocket socket) throws Exception { + private void processMessage(String clientId, byte[] payload, ServerWebSocket socket) throws Exception { // 1.1 基础检查 - if (StrUtil.isBlank(message)) { + if (ArrayUtil.isEmpty(payload)) { return; } - // 1.2 解码消息(已认证连接使用其 codecType,未认证连接使用默认 CODEC_TYPE) - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - String codecType = connectionInfo != null ? connectionInfo.getCodecType() : CODEC_TYPE; + // 1.2 解码消息 IotDeviceMessage deviceMessage; try { - deviceMessage = deviceMessageService.decodeDeviceMessage( - StrUtil.utf8Bytes(message), codecType); + deviceMessage = serializer.deserialize(payload); if (deviceMessage == null) { throw new Exception("解码后消息为空"); } @@ -132,6 +173,7 @@ public class IotWebSocketUpstreamHandler implements Handler { handleBusinessRequest(clientId, deviceMessage, socket); } } catch (Exception e) { + // TODO @AI:参考 IotTcpUpstreamHandler 处理;业务、参数、其它 log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", clientId, deviceMessage.getMethod(), e); // 发送错误响应,避免客户端一直等待 @@ -153,6 +195,7 @@ public class IotWebSocketUpstreamHandler implements Handler { private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { try { // 1.1 解析认证参数 + // TODO @AI:参数解析;参考 tcp 对应的 handleAuthenticationRequest IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); if (authParams == null) { log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId); @@ -204,6 +247,7 @@ public class IotWebSocketUpstreamHandler implements Handler { * @see 阿里云 - 一型一密 */ private void handleRegisterRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { + // TODO @AI:参数解析;参考 tcp 对应的 handleRegisterRequest try { // 1. 解析注册参数 IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); @@ -232,6 +276,7 @@ public class IotWebSocketUpstreamHandler implements Handler { } } + // TODO @AI:参考对应的 tcp 的 handleBusinessRequest /** * 处理业务请求 * @@ -270,10 +315,7 @@ public class IotWebSocketUpstreamHandler implements Handler { IotWebSocketConnectionManager.ConnectionInfo connectionInfo = new IotWebSocketConnectionManager.ConnectionInfo() .setDeviceId(device.getId()) .setProductKey(device.getProductKey()) - .setDeviceName(device.getDeviceName()) - .setClientId(clientId) - .setCodecType(CODEC_TYPE); - // 注册连接 + .setDeviceName(device.getDeviceName()); connectionManager.registerConnection(socket, device.getId(), connectionInfo); } @@ -314,6 +356,8 @@ public class IotWebSocketUpstreamHandler implements Handler { } } + // ===================== 发送响应消息 ===================== + /** * 发送响应消息 * @@ -332,8 +376,7 @@ public class IotWebSocketUpstreamHandler implements Handler { int code = success ? 0 : 401; IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, code, message); - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); - socket.writeTextMessage(StrUtil.utf8Str(encodedData)); + writeResponse(socket, responseMessage); } catch (Exception e) { log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); } @@ -461,11 +504,22 @@ public class IotWebSocketUpstreamHandler implements Handler { IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); // 2. 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); - socket.writeTextMessage(StrUtil.utf8Str(encodedData)); + writeResponse(socket, responseMessage); } catch (Exception e) { log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e); } } + /** + * 写入响应消息 + */ + private void writeResponse(ServerWebSocket socket, IotDeviceMessage responseMessage) { + byte[] payload = serializer.serialize(responseMessage); + if (binaryPayload) { + socket.writeBinaryMessage(Buffer.buffer(payload)); + } else { + socket.writeTextMessage(StrUtil.utf8Str(payload)); + } + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java index 128b360086..1477406450 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java @@ -1,10 +1,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager; +import io.vertx.core.buffer.Buffer; import io.vertx.core.http.ServerWebSocket; import lombok.Data; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -20,7 +20,6 @@ import java.util.concurrent.ConcurrentHashMap; * @author 芋道源码 */ @Slf4j -@Component public class IotWebSocketConnectionManager { /** @@ -69,7 +68,8 @@ public class IotWebSocketConnectionManager { return; } Long deviceId = connectionInfo.getDeviceId(); - deviceSocketMap.remove(deviceId); + // 仅当 deviceSocketMap 中的 socket 是当前 socket 时才移除,避免误删新连接 + deviceSocketMap.remove(deviceId, socket); log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress()); } @@ -115,6 +115,33 @@ public class IotWebSocketConnectionManager { } } + // TODO @AI:没必要这里加一个; + /** + * 发送消息到设备(二进制消息) + * + * @param deviceId 设备 ID + * @param payload 二进制消息 + * @return 是否发送成功 + */ + public boolean sendToDevice(Long deviceId, byte[] payload) { + ServerWebSocket socket = deviceSocketMap.get(deviceId); + if (socket == null) { + log.warn("[sendToDevice][设备未连接,设备 ID: {}]", deviceId); + return false; + } + + try { + socket.writeBinaryMessage(Buffer.buffer(payload)); + log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, payload.length); + return true; + } catch (Exception e) { + log.error("[sendToDevice][发送消息失败,设备 ID: {}]", deviceId, e); + // 发送失败时清理连接 + unregisterConnection(socket); + return false; + } + } + /** * 连接信息(包含认证信息) */ @@ -135,15 +162,6 @@ public class IotWebSocketConnectionManager { */ private String deviceName; - /** - * 客户端 ID - */ - private String clientId; - /** - * 消息编解码类型(认证后确定) - */ - private String codecType; - } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index ddc353f399..add4dce6a8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -94,7 +94,7 @@ yudao: - id: websocket-json type: websocket port: 8094 - enabled: false + enabled: true serialize: json websocket: path: /ws @@ -159,6 +159,7 @@ yudao: max-message-size: 8192 connect-timeout-seconds: 60 ssl-enabled: false + --- #################### 日志相关配置 #################### # 基础日志配置 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java index 4f3fe2daf2..192dce359c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java @@ -270,7 +270,7 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { }); socket.handler(parser); - // 2.1 序列化 + 帧编码(复用 gateway 的编码逻辑) + // 2.1 序列化 + 帧编码 byte[] serializedData = SERIALIZER.serialize(request); Buffer frameData = FRAME_CODEC.encode(serializedData); log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length()); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java index 2efbd4d677..5bb113b919 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java @@ -373,7 +373,7 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { }); socket.handler(parser); - // 2.1 序列化 + 帧编码(复用 gateway 的编码逻辑) + // 2.1 序列化 + 帧编码 byte[] serializedData = SERIALIZER.serialize(request); Buffer frameData = FRAME_CODEC.encode(serializedData); log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length()); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java index 1980d0a08c..22b654a869 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java @@ -249,7 +249,7 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { }); socket.handler(parser); - // 2.1 序列化 + 帧编码(复用 gateway 的编码逻辑) + // 2.1 序列化 + 帧编码 byte[] serializedData = SERIALIZER.serialize(request); Buffer frameData = FRAME_CODEC.encode(serializedData); log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length()); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java index ca79c4220c..15eed61e2a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java @@ -10,8 +10,8 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.vertx.core.Vertx; import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketClient; @@ -61,7 +61,7 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { // ===================== 编解码器选择 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== @@ -95,10 +95,10 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testAuth][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 2.1 创建 WebSocket 连接(同步) WebSocket ws = createWebSocketConnection(); @@ -109,7 +109,7 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { // 3. 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testAuth][响应消息: {}]", responseMessage); } else { log.warn("[testAuth][未收到响应]"); @@ -137,10 +137,10 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { registerReqDTO.setProductSecret("test-product-secret"); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testDeviceRegister][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 2.1 创建 WebSocket 连接(同步) WebSocket ws = createWebSocketConnection(); @@ -151,7 +151,7 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { // 3. 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testDeviceRegister][响应消息: {}]", responseMessage); log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); } else { @@ -186,16 +186,16 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { .put("height", "2") .build()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testPropertyPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testPropertyPost][响应消息: {}]", responseMessage); } else { log.warn("[testPropertyPost][未收到响应]"); @@ -229,16 +229,16 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { MapUtil.builder().put("rice", 3).build(), System.currentTimeMillis()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testEventPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testEventPost][响应消息: {}]", responseMessage); } else { log.warn("[testEventPost][未收到响应]"); @@ -308,13 +308,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); log.info("[authenticate][发送认证请求: {}]", jsonMessage); String response = sendAndReceive(ws, jsonMessage); if (response != null) { - return CODEC.decode(StrUtil.utf8Bytes(response)); + return SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); } return null; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java index a44f2f6dd5..20d66fa0a7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java @@ -14,8 +14,8 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.vertx.core.Vertx; import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketClient; @@ -67,9 +67,9 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器选择 ===================== + // ===================== 序列化器选择 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== @@ -110,10 +110,10 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testAuth][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 2.1 创建 WebSocket 连接(同步) WebSocket ws = createWebSocketConnection(); @@ -124,7 +124,7 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { // 3. 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testAuth][响应消息: {}]", responseMessage); } else { log.warn("[testAuth][未收到响应]"); @@ -164,16 +164,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), params, null, null, null); - // 2.3 编码 - byte[] payload = CODEC.encode(request); + // 2.3 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testTopoAdd][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testTopoAdd][响应消息: {}]", responseMessage); } else { log.warn("[testTopoAdd][未收到响应]"); @@ -205,16 +205,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), params, null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testTopoDelete][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testTopoDelete][响应消息: {}]", responseMessage); } else { log.warn("[testTopoDelete][未收到响应]"); @@ -244,16 +244,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), params, null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testTopoGet][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testTopoGet][响应消息: {}]", responseMessage); } else { log.warn("[testTopoGet][未收到响应]"); @@ -287,16 +287,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), Collections.singletonList(subDevice), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testSubDeviceRegister][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testSubDeviceRegister][响应消息: {}]", responseMessage); } else { log.warn("[testSubDeviceRegister][未收到响应]"); @@ -358,16 +358,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), params, null, null, null); - // 2.7 编码 - byte[] payload = CODEC.encode(request); + // 2.7 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testPropertyPackPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testPropertyPackPost][响应消息: {}]", responseMessage); } else { log.warn("[testPropertyPackPost][未收到响应]"); @@ -438,13 +438,13 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); log.info("[authenticate][发送认证请求: {}]", jsonMessage); String response = sendAndReceive(ws, jsonMessage); if (response != null) { - return CODEC.decode(StrUtil.utf8Bytes(response)); + return SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); } return null; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java index 04bf3d5632..f792288fe3 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java @@ -9,8 +9,8 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.vertx.core.Vertx; import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketClient; @@ -60,9 +60,9 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器选择 ===================== + // ===================== 序列化器选择 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -96,10 +96,10 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testAuth][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 2.1 创建 WebSocket 连接(同步) WebSocket ws = createWebSocketConnection(); @@ -110,7 +110,7 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { // 3. 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testAuth][响应消息: {}]", responseMessage); } else { log.warn("[testAuth][未收到响应]"); @@ -146,16 +146,16 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { .put("temperature", 36.5) .build()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testPropertyPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testPropertyPost][响应消息: {}]", responseMessage); } else { log.warn("[testPropertyPost][未收到响应]"); @@ -195,16 +195,16 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { .build(), System.currentTimeMillis()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testEventPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testEventPost][响应消息: {}]", responseMessage); } else { log.warn("[testEventPost][未收到响应]"); @@ -274,13 +274,13 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); log.info("[authenticate][发送认证请求: {}]", jsonMessage); String response = sendAndReceive(ws, jsonMessage); if (response != null) { - return CODEC.decode(StrUtil.utf8Bytes(response)); + return SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); } return null; } From c105ecea96e0ba864ba6e3100d6697cd66c480d2 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 19:26:05 +0800 Subject: [PATCH 18/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91websocket?= =?UTF-8?q?=20=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=88100%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gateway/config/IotGatewayProperties.java | 4 +- .../upstream/IotTcpUpstreamHandler.java | 34 +- .../websocket/IotWebSocketProtocol.java | 27 +- .../IotWebSocketDownstreamHandler.java | 25 +- .../upstream/IotWebSocketUpstreamHandler.java | 457 +++++------------- .../IotWebSocketConnectionManager.java | 28 -- 6 files changed, 151 insertions(+), 424 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index dc5d545373..ce1616132b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -291,7 +291,7 @@ public class IotGatewayProperties { */ private Integer keepAliveTimeoutSeconds = 300; - // TODO @AI:所有跟 ssl 相关的参数,是不是可以统一?放到 protocol 层级?ProtocolInstanceProperties【优先级:低】暂时不用规划; + // NOTE:SSL 相关参数后续统一到 protocol 层级(优先级低) /** * 是否启用 SSL */ @@ -336,7 +336,7 @@ public class IotGatewayProperties { } - // TODO @AI:【暂时忽略】改成 ProtocolProperties + // NOTE:暂未统一为 ProtocolProperties,待协议改造完成再调整 /** * 协议实例配置 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java index 9542b504c2..93fadd8bbe 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream; import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.IdUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -79,35 +78,32 @@ public class IotTcpUpstreamHandler implements Handler { @Override @SuppressWarnings("DuplicatedCode") public void handle(NetSocket socket) { - // TODO @AI:clientId 去掉;其它模块也看看,怎么去掉下看看; - String clientId = IdUtil.simpleUUID(); - log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + String remoteAddress = String.valueOf(socket.remoteAddress()); + log.debug("[handle][设备连接,地址: {}]", remoteAddress); // 1. 设置异常和关闭处理器 socket.exceptionHandler(ex -> { - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + log.warn("[handle][连接异常,地址: {}]", remoteAddress, ex); socket.close(); }); socket.closeHandler(v -> { - log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + log.debug("[handle][连接关闭,地址: {}]", remoteAddress); cleanupConnection(socket); }); // 2.1 设置消息处理器 - // TODO @AI:去掉 clientId; Handler messageHandler = buffer -> { try { processMessage(buffer, socket); } catch (Exception e) { - log.error("[handle][消息处理失败,客户端 ID: {},地址: {}]", - clientId, socket.remoteAddress(), e); + log.error("[handle][消息处理失败,地址: {}]", remoteAddress, e); socket.close(); } }; // 2.2 使用拆包器处理粘包/拆包 RecordParser parser = codec.createDecodeParser(messageHandler); socket.handler(parser); - log.debug("[handle][启用 {} 拆包器,客户端 ID: {}]", codec.getType(), clientId); + log.debug("[handle][启用 {} 拆包器,地址: {}]", codec.getType(), remoteAddress); } /** @@ -135,23 +131,23 @@ public class IotTcpUpstreamHandler implements Handler { handleRegisterRequest(message, socket); } else { // 业务消息 - handleBusinessRequest(null, message, socket); + handleBusinessRequest(message, socket); } } catch (ServiceException e) { // 业务异常,返回对应的错误码和错误信息 - log.warn("[processMessage][业务异常,客户端 ID: {},错误: {}]", null, e.getMessage()); + log.warn("[processMessage][业务异常,地址: {},错误: {}]", socket.remoteAddress(), e.getMessage()); String requestId = message != null ? message.getRequestId() : null; String method = message != null ? message.getMethod() : null; sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage()); } catch (IllegalArgumentException e) { // 参数校验失败,返回 400 - log.warn("[processMessage][参数校验失败,客户端 ID: {},错误: {}]", null, e.getMessage()); + log.warn("[processMessage][参数校验失败,地址: {},错误: {}]", socket.remoteAddress(), e.getMessage()); String requestId = message != null ? message.getRequestId() : null; String method = message != null ? message.getMethod() : null; sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage()); } catch (Exception e) { // 其他异常,返回 500,并重新抛出让上层关闭连接 - log.error("[processMessage][处理消息失败,客户端 ID: {}]", null, e); + log.error("[processMessage][处理消息失败,地址: {}]", socket.remoteAddress(), e); String requestId = message != null ? message.getRequestId() : null; String method = message != null ? message.getMethod() : null; sendErrorResponse(socket, requestId, method, @@ -218,21 +214,21 @@ public class IotTcpUpstreamHandler implements Handler { // 3. 发送成功响应 sendSuccessResponse(socket, message.getRequestId(), IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); - log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", null, params.getDeviceName()); + log.info("[handleRegisterRequest][注册成功,地址: {},设备名: {}]", + socket.remoteAddress(), params.getDeviceName()); } /** * 处理业务请求 * - * @param clientId 客户端 ID * @param message 消息信息 * @param socket 网络连接 */ - private void handleBusinessRequest(String clientId, IotDeviceMessage message, NetSocket socket) { + private void handleBusinessRequest(IotDeviceMessage message, NetSocket socket) { // 1. 获取认证信息并处理业务消息 IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); if (connectionInfo == null) { - log.error("[handleBusinessRequest][无法获取连接信息,客户端 ID: {}]", clientId); + log.error("[handleBusinessRequest][无法获取连接信息,地址: {}]", socket.remoteAddress()); sendErrorResponse(socket, message.getRequestId(), message.getMethod(), UNAUTHORIZED.getCode(), "设备未认证,无法处理业务消息"); return; @@ -241,7 +237,7 @@ public class IotTcpUpstreamHandler implements Handler { // 2. 发送消息到消息总线 deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), connectionInfo.getDeviceName(), serverId); - log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}", clientId, message); + log.info("[handleBusinessRequest][发送消息到消息总线,地址: {},消息: {}]", socket.remoteAddress(), message); } /** diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java index 112c5acf5c..67d5608936 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java @@ -9,13 +9,11 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.upstream.IotWebSocketUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; @@ -24,9 +22,10 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.util.Assert; -// TODO @AI:注释调整下,参考 IotTcpProtocol /** - * IoT 网关 WebSocket 协议:接收设备上行消息 + * IoT WebSocket 协议实现 + *

                              + * 基于 Vert.x 实现 WebSocket 服务器,接收设备上行消息 * * @author 芋道源码 */ @@ -62,12 +61,6 @@ public class IotWebSocketProtocol implements IotProtocol { */ private final IotWebSocketConnectionManager connectionManager; - // TODO @AI:可以不用这个变量,从 properties 里面获取 - /** - * WebSocket 配置 - */ - private final IotWebSocketConfig wsConfig; - /** * 下行消息订阅者 */ @@ -78,14 +71,10 @@ public class IotWebSocketProtocol implements IotProtocol { */ private final IotMessageSerializer serializer; - private final IotDeviceService deviceService; - private final IotDeviceMessageService messageService; - public IotWebSocketProtocol(ProtocolInstanceProperties properties) { Assert.notNull(properties, "协议实例配置不能为空"); Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空"); this.properties = properties; - this.wsConfig = properties.getWebsocket(); this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); // 初始化序列化器 @@ -94,9 +83,7 @@ public class IotWebSocketProtocol implements IotProtocol { IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); this.serializer = serializerManager.get(serializeType); - // 初始化基础依赖 - this.deviceService = SpringUtil.getBean(IotDeviceService.class); - this.messageService = SpringUtil.getBean(IotDeviceMessageService.class); + // 初始化连接管理器 this.connectionManager = new IotWebSocketConnectionManager(); // 初始化下行消息订阅者 @@ -127,6 +114,7 @@ public class IotWebSocketProtocol implements IotProtocol { this.vertx = Vertx.vertx(); // 1.2 创建服务器选项 + IotWebSocketConfig wsConfig = properties.getWebsocket(); HttpServerOptions options = new HttpServerOptions() .setPort(properties.getPort()) .setIdleTimeout(wsConfig.getIdleTimeoutSeconds()) @@ -150,8 +138,7 @@ public class IotWebSocketProtocol implements IotProtocol { return; } // 创建上行处理器 - IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(this, - messageService, deviceService, connectionManager, serializer); + IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(serverId, serializer, connectionManager); handler.handle(socket); }); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamHandler.java index 096435eacb..6391fd42fb 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamHandler.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstrea import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import lombok.RequiredArgsConstructor; @@ -37,23 +36,17 @@ public class IotWebSocketDownstreamHandler { return; } - // 2. 编码消息并发送到设备 + // 2. 序列化 byte[] bytes = serializer.serialize(message); - // TODO @AI:参考别的模块的做法,直接发?类似 tcp 这种; - boolean success; - if (serializer.getType() == IotSerializeTypeEnum.BINARY) { - success = connectionManager.sendToDevice(message.getDeviceId(), bytes); - } else { - String jsonMessage = StrUtil.utf8Str(bytes); - success = connectionManager.sendToDevice(message.getDeviceId(), jsonMessage); - } - if (success) { - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); - } else { - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); + String bytesContent = StrUtil.utf8Str(bytes); + + // 3. 发送到设备 + boolean success = connectionManager.sendToDevice(connectionInfo.getDeviceId(), bytesContent); + if (!success) { + throw new RuntimeException("下行消息发送失败"); } + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); } catch (Exception e) { log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", message.getDeviceId(), message.getMethod(), message, e); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java index 2b6b5e4fbe..c838198115 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java @@ -1,34 +1,33 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.upstream; -import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Handler; -import io.vertx.core.buffer.Buffer; import io.vertx.core.http.ServerWebSocket; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; -import java.util.Map; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; /** @@ -52,256 +51,175 @@ public class IotWebSocketUpstreamHandler implements Handler { */ private final IotWebSocketConnectionManager connectionManager; - // TODO @AI:是不是可以去掉? - private final boolean binaryPayload; - private final IotDeviceMessageService deviceMessageService; private final IotDeviceService deviceService; private final IotDeviceCommonApi deviceApi; - // TODO @AI:参数、顺序参考 IotTcpUpstreamHandler - public IotWebSocketUpstreamHandler(IotWebSocketProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotDeviceService deviceService, - IotWebSocketConnectionManager connectionManager, - IotMessageSerializer serializer) { - this.deviceMessageService = deviceMessageService; - this.deviceService = deviceService; - this.connectionManager = connectionManager; + public IotWebSocketUpstreamHandler(String serverId, + IotMessageSerializer serializer, + IotWebSocketConnectionManager connectionManager) { + this.serverId = serverId; this.serializer = serializer; - this.binaryPayload = serializer.getType() == IotSerializeTypeEnum.BINARY; + this.connectionManager = connectionManager; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.deviceService = SpringUtil.getBean(IotDeviceService.class); this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.serverId = protocol.getServerId(); - // TODO @AI:通过 springutil;deviceService、deviceMessageService; } @Override @SuppressWarnings("DuplicatedCode") public void handle(ServerWebSocket socket) { - String clientId = IdUtil.simpleUUID(); - log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + String remoteAddress = String.valueOf(socket.remoteAddress()); + log.debug("[handle][设备连接,地址: {}]", remoteAddress); // 1. 设置异常和关闭处理器 - // TODO @AI:clientId 去掉; socket.exceptionHandler(ex -> { - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + log.warn("[handle][连接异常,地址: {}]", remoteAddress, ex); socket.close(); }); socket.closeHandler(v -> { - log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + log.debug("[handle][连接关闭,地址: {}]", remoteAddress); cleanupConnection(socket); }); - // 2. 设置消息处理器(JSON 使用文本,BINARY 使用二进制) - // TODO @AI:是不是 text、binary 保持统一?用一个 mesagehandler? - if (binaryPayload) { - socket.binaryMessageHandler(buffer -> { - try { - processMessage(clientId, buffer.getBytes(), socket); - } catch (Exception e) { - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", - clientId, socket.remoteAddress(), e.getMessage()); - cleanupConnection(socket); - socket.close(); - } - }); - socket.textMessageHandler(message -> { - log.warn("[handle][收到文本帧但当前序列化为 BINARY,断开连接,客户端 ID: {},地址: {}]", - clientId, socket.remoteAddress()); - cleanupConnection(socket); + // 2. 设置消息处理器(仅支持文本帧) + socket.textMessageHandler(message -> { + try { + processMessage(StrUtil.utf8Bytes(message), socket); + } catch (Exception e) { + log.error("[handle][消息解码失败,断开连接,地址: {},错误: {}]", remoteAddress, e.getMessage()); socket.close(); - }); - } else { - socket.textMessageHandler(message -> { - try { - processMessage(clientId, StrUtil.utf8Bytes(message), socket); - } catch (Exception e) { - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", - clientId, socket.remoteAddress(), e.getMessage()); - // TODO @AI:是不是不用 cleanupConnection?closehandler 本身就吹了了; - cleanupConnection(socket); - socket.close(); - } - }); - socket.binaryMessageHandler(buffer -> { - try { - processMessage(clientId, buffer.getBytes(), socket); - } catch (Exception e) { - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", - clientId, socket.remoteAddress(), e.getMessage()); - cleanupConnection(socket); - socket.close(); - } - }); - } + } + }); } /** * 处理消息 * - * @param clientId 客户端 ID * @param payload 消息负载 * @param socket WebSocket 连接 - * @throws Exception 消息解码失败时抛出异常 */ - private void processMessage(String clientId, byte[] payload, ServerWebSocket socket) throws Exception { - // 1.1 基础检查 - if (ArrayUtil.isEmpty(payload)) { - return; - } - // 1.2 解码消息 - IotDeviceMessage deviceMessage; + private void processMessage(byte[] payload, ServerWebSocket socket) { + IotDeviceMessage message = null; try { - deviceMessage = serializer.deserialize(payload); - if (deviceMessage == null) { - throw new Exception("解码后消息为空"); + // 1.1 基础检查 + if (ArrayUtil.isEmpty(payload)) { + return; } - } catch (Exception e) { - throw new Exception("消息解码失败: " + e.getMessage(), e); - } + // 1.2 解码消息 + message = serializer.deserialize(payload); + Assert.notNull(message, "消息反序列化失败"); + Assert.hasText(message.getMethod(), "method 不能为空"); - // 2. 根据消息类型路由处理 - try { - if (AUTH_METHOD.equals(deviceMessage.getMethod())) { - // 认证请求 - handleAuthenticationRequest(clientId, deviceMessage, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(deviceMessage.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(clientId, deviceMessage, socket); + // 2. 根据消息类型路由处理 + if (AUTH_METHOD.equals(message.getMethod())) { + handleAuthenticationRequest(message, socket); + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { + handleRegisterRequest(message, socket); } else { - // 业务消息 - handleBusinessRequest(clientId, deviceMessage, socket); + handleBusinessRequest(message, socket); } + } catch (ServiceException e) { + log.warn("[processMessage][业务异常,错误: {}]", e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + log.warn("[processMessage][参数校验失败,错误: {}]", e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage()); } catch (Exception e) { - // TODO @AI:参考 IotTcpUpstreamHandler 处理;业务、参数、其它 - log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", - clientId, deviceMessage.getMethod(), e); - // 发送错误响应,避免客户端一直等待 - try { - sendErrorResponse(socket, deviceMessage.getRequestId(), "消息处理失败"); - } catch (Exception responseEx) { - log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx); - } + log.error("[processMessage][处理消息失败]", e); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, INTERNAL_SERVER_ERROR.getCode(), + INTERNAL_SERVER_ERROR.getMsg()); + throw e; } } /** * 处理认证请求 * - * @param clientId 客户端 ID * @param message 消息信息 * @param socket WebSocket 连接 */ - private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { - try { - // 1.1 解析认证参数 - // TODO @AI:参数解析;参考 tcp 对应的 handleAuthenticationRequest - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); - if (authParams == null) { - log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "认证参数不完整"); - return; - } - // 1.2 执行认证 - if (!validateDeviceAuth(authParams)) { - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", - clientId, authParams.getUsername()); - sendErrorResponse(socket, message.getRequestId(), "认证失败"); - return; - } + @SuppressWarnings("DuplicatedCode") + private void handleAuthenticationRequest(IotDeviceMessage message, ServerWebSocket socket) { + // 1. 解析认证参数 + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); + Assert.notNull(authParams, "认证参数不能为空"); + Assert.hasText(authParams.getUsername(), "username 不能为空"); + Assert.hasText(authParams.getPassword(), "password 不能为空"); - // 2.1 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); - if (deviceInfo == null) { - sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败"); - return; - } - // 2.2 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - sendErrorResponse(socket, message.getRequestId(), "设备不存在"); - return; - } - - // 3.1 注册连接 - registerConnection(socket, device, clientId); - // 3.2 发送上线消息 - sendOnlineMessage(device); - // 3.3 发送成功响应 - sendSuccessResponse(socket, message.getRequestId(), "认证成功"); - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", - device.getId(), device.getDeviceName()); - } catch (Exception e) { - log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "认证处理异常"); + // 2.1 执行认证 + CommonResult authResult = deviceApi.authDevice(authParams); + authResult.checkError(); + if (BooleanUtil.isFalse(authResult.getData())) { + throw exception(DEVICE_AUTH_FAIL); } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + + // 3.1 注册连接 + registerConnection(socket, device); + // 3.2 发送上线消息 + sendOnlineMessage(device); + // 3.3 发送成功响应 + sendSuccessResponse(socket, message.getRequestId(), AUTH_METHOD, "认证成功"); + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", device.getId(), device.getDeviceName()); } /** * 处理设备动态注册请求(一型一密,不需要认证) * - * @param clientId 客户端 ID * @param message 消息信息 * @param socket WebSocket 连接 * @see 阿里云 - 一型一密 */ - private void handleRegisterRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { - // TODO @AI:参数解析;参考 tcp 对应的 handleRegisterRequest - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null - || StrUtil.hasEmpty(params.getProductKey(), params.getDeviceName(), params.getProductSecret())) { - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "注册参数不完整"); - return; - } + @SuppressWarnings("DuplicatedCode") + private void handleRegisterRequest(IotDeviceMessage message, ServerWebSocket socket) { + // 1. 解析注册参数 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); + Assert.notNull(params, "注册参数不能为空"); + Assert.hasText(params.getProductKey(), "productKey 不能为空"); + Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); - // 2. 调用动态注册 - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendErrorResponse(socket, message.getRequestId(), result.getMsg()); - return; - } + // 2. 调用动态注册 + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData()); - log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", - clientId, params.getDeviceName()); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "注册处理异常"); - } + // 3. 发送成功响应(包含 deviceSecret) + sendSuccessResponse(socket, message.getRequestId(), + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); + log.info("[handleRegisterRequest][注册成功,设备名: {}]", params.getDeviceName()); } - // TODO @AI:参考对应的 tcp 的 handleBusinessRequest /** * 处理业务请求 * - * @param clientId 客户端 ID * @param message 消息信息 * @param socket WebSocket 连接 */ - private void handleBusinessRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { - try { - // 1. 获取认证信息并处理业务消息 - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo == null) { - log.warn("[handleBusinessRequest][连接未认证,拒绝处理业务消息,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "连接未认证"); - return; - } - - // 2. 发送消息到消息总线 - deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}", - clientId, message.toString()); - } catch (Exception e) { - log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e); + private void handleBusinessRequest(IotDeviceMessage message, ServerWebSocket socket) { + // 1. 获取认证信息并处理业务消息 + IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo == null) { + log.warn("[handleBusinessRequest][连接未认证,拒绝处理业务消息]"); + sendErrorResponse(socket, message.getRequestId(), message.getMethod(), + UNAUTHORIZED.getCode(), "设备未认证,无法处理业务消息"); + return; } + + // 2. 发送消息到消息总线 + deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + log.info("[handleBusinessRequest][发送消息到消息总线,消息: {}]", message); } /** @@ -309,9 +227,8 @@ public class IotWebSocketUpstreamHandler implements Handler { * * @param socket WebSocket 连接 * @param device 设备 - * @param clientId 客户端 ID */ - private void registerConnection(ServerWebSocket socket, IotDeviceRespDTO device, String clientId) { + private void registerConnection(ServerWebSocket socket, IotDeviceRespDTO device) { IotWebSocketConnectionManager.ConnectionInfo connectionInfo = new IotWebSocketConnectionManager.ConnectionInfo() .setDeviceId(device.getId()) .setProductKey(device.getProductKey()) @@ -362,152 +279,18 @@ public class IotWebSocketUpstreamHandler implements Handler { * 发送响应消息 * * @param socket WebSocket 连接 - * @param success 是否成功 - * @param message 消息 * @param requestId 请求 ID + * @param method 请求方法 + * @param data 响应数据 */ - private void sendResponse(ServerWebSocket socket, boolean success, String message, String requestId) { - try { - Object responseData = MapUtil.builder() - .put("success", success) - .put("message", message) - .build(); - - int code = success ? 0 : 401; - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, code, message); - - writeResponse(socket, responseMessage); - } catch (Exception e) { - log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); - } + private void sendSuccessResponse(ServerWebSocket socket, String requestId, String method, Object data) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null); + writeResponse(socket, responseMessage); } - /** - * 验证设备认证信息 - * - * @param authParams 认证参数 - * @return 是否认证成功 - */ - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) - .setPassword(authParams.getPassword())); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); - return false; - } - } - - /** - * 发送错误响应 - * - * @param socket WebSocket 连接 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - */ - private void sendErrorResponse(ServerWebSocket socket, String requestId, String errorMessage) { - sendResponse(socket, false, errorMessage, requestId); - } - - /** - * 发送成功响应 - * - * @param socket WebSocket 连接 - * @param requestId 请求 ID - * @param message 消息 - */ - @SuppressWarnings("SameParameterValue") - private void sendSuccessResponse(ServerWebSocket socket, String requestId, String message) { - sendResponse(socket, true, message, requestId); - } - - /** - * 解析认证参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 认证参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceAuthReqDTO parseAuthParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceAuthReqDTO() - .setClientId(MapUtil.getStr(paramMap, "clientId")) - .setUsername(MapUtil.getStr(paramMap, "username")) - .setPassword(MapUtil.getStr(paramMap, "password")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceAuthReqDTO) { - return (IotDeviceAuthReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); - } catch (Exception e) { - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); - return null; - } - } - - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings("unchecked") - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceRegisterReqDTO() - .setProductKey(MapUtil.getStr(paramMap, "productKey")) - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceRegisterReqDTO) { - return (IotDeviceRegisterReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param socket WebSocket 连接 - * @param requestId 请求 ID - * @param registerResp 注册响应 - */ - private void sendRegisterSuccessResponse(ServerWebSocket socket, String requestId, - IotDeviceRegisterRespDTO registerResp) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - // 2. 发送响应 - writeResponse(socket, responseMessage); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e); - } + private void sendErrorResponse(ServerWebSocket socket, String requestId, String method, Integer code, String msg) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg); + writeResponse(socket, responseMessage); } /** @@ -515,11 +298,7 @@ public class IotWebSocketUpstreamHandler implements Handler { */ private void writeResponse(ServerWebSocket socket, IotDeviceMessage responseMessage) { byte[] payload = serializer.serialize(responseMessage); - if (binaryPayload) { - socket.writeBinaryMessage(Buffer.buffer(payload)); - } else { - socket.writeTextMessage(StrUtil.utf8Str(payload)); - } + socket.writeTextMessage(StrUtil.utf8Str(payload)); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java index 1477406450..8b09da0f98 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager; -import io.vertx.core.buffer.Buffer; import io.vertx.core.http.ServerWebSocket; import lombok.Data; import lombok.experimental.Accessors; @@ -115,33 +114,6 @@ public class IotWebSocketConnectionManager { } } - // TODO @AI:没必要这里加一个; - /** - * 发送消息到设备(二进制消息) - * - * @param deviceId 设备 ID - * @param payload 二进制消息 - * @return 是否发送成功 - */ - public boolean sendToDevice(Long deviceId, byte[] payload) { - ServerWebSocket socket = deviceSocketMap.get(deviceId); - if (socket == null) { - log.warn("[sendToDevice][设备未连接,设备 ID: {}]", deviceId); - return false; - } - - try { - socket.writeBinaryMessage(Buffer.buffer(payload)); - log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, payload.length); - return true; - } catch (Exception e) { - log.error("[sendToDevice][发送消息失败,设备 ID: {}]", deviceId, e); - // 发送失败时清理连接 - unregisterConnection(socket); - return false; - } - } - /** * 连接信息(包含认证信息) */ From 3a26b2e39f749bae6ab410209dcc3e762d6b2427 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 19:33:47 +0800 Subject: [PATCH 19/53] =?UTF-8?q?feat=EF=BC=9A=E3=80=90iot=E3=80=91?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E6=9C=80=E6=96=B0=20protocol=20=E6=9C=80?= =?UTF-8?q?=E6=96=B0=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/iot/gateway/config/IotGatewayConfiguration.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index 0eebd894da..190d43a319 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -5,8 +5,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.*; import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; From 1e2dc281e3ad2f7064eb0a857e2a0bc9ece9733a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 20:56:56 +0800 Subject: [PATCH 20/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91mqtt=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=8820%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/IotGatewayConfiguration.java | 42 -- .../gateway/config/IotGatewayProperties.java | 90 +-- .../gateway/protocol/IotProtocolManager.java | 13 + .../gateway/protocol/mqtt/IotMqttConfig.java | 44 ++ .../protocol/mqtt/IotMqttProtocol.java | 317 +++++++++++ .../mqtt/IotMqttUpstreamProtocol.java | 117 ---- .../downstream}/IotMqttDownstreamHandler.java | 23 +- .../IotMqttDownstreamSubscriber.java | 7 +- .../upstream/IotMqttAbstractHandler.java | 105 ++++ .../upstream/IotMqttConnectionHandler.java | 178 ++++++ .../upstream/IotMqttRegisterHandler.java | 182 +++++++ .../upstream/IotMqttUpstreamHandler.java | 81 +++ .../manager/IotMqttConnectionManager.java | 4 +- .../mqtt/router/IotMqttUpstreamHandler.java | 511 ------------------ .../downstream/IotTcpDownstreamHandler.java | 4 +- .../src/main/resources/application.yaml | 30 +- 16 files changed, 964 insertions(+), 784 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/{router => handler/downstream}/IotMqttDownstreamHandler.java (89%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/{ => handler/downstream}/IotMqttDownstreamSubscriber.java (82%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttConnectionHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index e9800c34e4..2115f76c02 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -5,12 +5,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; @@ -67,41 +62,4 @@ public class IotGatewayConfiguration { } } - /** - * IoT 网关 MQTT 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true") - @Slf4j - public static class MqttProtocolConfiguration { - - @Bean(name = "mqttVertx", destroyMethod = "close") - public Vertx mqttVertx() { - return Vertx.vertx(); - } - - @Bean - public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceMessageService messageService, - IotMqttConnectionManager connectionManager, - @Qualifier("mqttVertx") Vertx mqttVertx) { - return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService, - connectionManager, mqttVertx); - } - - @Bean - public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService, - IotMqttConnectionManager connectionManager) { - return new IotMqttDownstreamHandler(messageService, connectionManager); - } - - @Bean - public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol, - IotMqttDownstreamHandler downstreamHandler, - IotMessageBus messageBus) { - return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus); - } - - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index ce1616132b..8cb6595b55 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -1,13 +1,11 @@ package cn.iocoder.yudao.module.iot.gateway.config; -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig; -import io.vertx.core.net.KeyCertOptions; -import io.vertx.core.net.TrustOptions; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -87,11 +85,6 @@ public class IotGatewayProperties { */ private EmqxProperties emqx; - /** - * MQTT 组件配置 - */ - private MqttProperties mqtt; - } @Data @@ -263,79 +256,6 @@ public class IotGatewayProperties { } - @Data - public static class MqttProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务器端口 - */ - private Integer port = 1883; - - /** - * 最大消息大小(字节) - */ - private Integer maxMessageSize = 8192; - - /** - * 连接超时时间(秒) - */ - private Integer connectTimeoutSeconds = 60; - /** - * 保持连接超时时间(秒) - */ - private Integer keepAliveTimeoutSeconds = 300; - - // NOTE:SSL 相关参数后续统一到 protocol 层级(优先级低) - /** - * 是否启用 SSL - */ - private Boolean sslEnabled = false; - /** - * SSL 配置 - */ - private SslOptions sslOptions = new SslOptions(); - - /** - * SSL 配置选项 - */ - @Data - public static class SslOptions { - - /** - * 密钥证书选项 - */ - private KeyCertOptions keyCertOptions; - /** - * 信任选项 - */ - private TrustOptions trustOptions; - /** - * SSL 证书路径 - */ - private String certPath; - /** - * SSL 私钥路径 - */ - private String keyPath; - /** - * 信任存储路径 - */ - private String trustStorePath; - /** - * 信任存储密码 - */ - private String trustStorePassword; - - } - - } - // NOTE:暂未统一为 ProtocolProperties,待协议改造完成再调整 /** * 协议实例配置 @@ -376,6 +296,8 @@ public class IotGatewayProperties { */ private String serialize; + // ========== 各协议配置 ========== + /** * HTTP 协议配置 */ @@ -406,6 +328,12 @@ public class IotGatewayProperties { @Valid private IotWebSocketConfig websocket; + /** + * MQTT 协议配置 + */ + @Valid + private IotMqttConfig mqtt; + } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index 45b6789041..47b41a3e28 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; @@ -106,6 +107,8 @@ public class IotProtocolManager implements SmartLifecycle { return createCoapProtocol(config); case WEBSOCKET: return createWebSocketProtocol(config); + case MQTT: + return createMqttProtocol(config); default: throw new IllegalArgumentException(String.format( "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); @@ -162,4 +165,14 @@ public class IotProtocolManager implements SmartLifecycle { return new IotWebSocketProtocol(config); } + /** + * 创建 MQTT 协议实例 + * + * @param config 协议实例配置 + * @return MQTT 协议实例 + */ + private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + return new IotMqttProtocol(config); + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java new file mode 100644 index 0000000000..5fb7f779fe --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; + +import lombok.Data; + +// TODO @AI:validator 参数校验。也看看其他几个配置类有没有类似问题 +/** + * IoT 网关 MQTT 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotMqttConfig { + + /** + * 是否启用 SSL + */ + private Boolean sslEnabled = false; + + /** + * SSL 证书路径 + */ + private String sslCertPath; + + /** + * SSL 私钥路径 + */ + private String sslKeyPath; + + /** + * 最大消息大小(字节) + */ + private Integer maxMessageSize = 8192; + + /** + * 连接超时时间(秒) + */ + private Integer connectTimeoutSeconds = 60; + + /** + * 保持连接超时时间(秒) + */ + private Integer keepAliveTimeoutSeconds = 300; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java new file mode 100644 index 0000000000..a8d8cb28d9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java @@ -0,0 +1,317 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttConnectionHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Vertx; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.mqtt.MqttEndpoint; +import io.vertx.mqtt.MqttServer; +import io.vertx.mqtt.MqttServerOptions; +import io.vertx.mqtt.MqttTopicSubscription; +import io.vertx.mqtt.messages.MqttPublishMessage; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * IoT 网关 MQTT 协议:接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolInstanceProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * MQTT 服务器 + */ + private MqttServer mqttServer; + /** + * 连接管理器 + */ + private IotMqttConnectionManager connectionManager; + + /** + * 下行消息订阅者 + */ + private IotMqttDownstreamSubscriber downstreamSubscriber; + + // TODO @AI:这个是不是提前创建下?因为是无状态的。 + private IotMqttConnectionHandler connectionHandler; + private IotMqttRegisterHandler registerHandler; + private IotMqttUpstreamHandler upstreamHandler; + + public IotMqttProtocol(ProtocolInstanceProperties properties) { + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // TODO @AI:初始化连接器,参考 IotTcpProtocol + + // TODO @AI:初始化下行消息订阅者,参考 IotTcpProtocol + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.MQTT; + } + + // TODO @AI:这个方法的整体注释风格,参考 IotTcpProtocol 的 start 方法。 + @Override + public void start() { + if (running) { + log.warn("[start][IoT MQTT 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例(每个 Protocol 独立管理) + this.vertx = Vertx.vertx(); + + // 1.2 创建连接管理器 + this.connectionManager = new IotMqttConnectionManager(); + + // 1.3 初始化 Handler + initHandlers(); + + // 2. 创建服务器选项 + IotMqttConfig mqttConfig = properties.getMqtt(); + // TODO @AI:default 值,在 IotMqttConfig 处理; + MqttServerOptions options = new MqttServerOptions() + .setPort(properties.getPort()) + .setMaxMessageSize(mqttConfig != null ? mqttConfig.getMaxMessageSize() : 8192) + .setTimeoutOnConnect(mqttConfig != null ? mqttConfig.getConnectTimeoutSeconds() : 60); + + // 3. 配置 SSL(如果启用) + if (mqttConfig != null && Boolean.TRUE.equals(mqttConfig.getSslEnabled())) { + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(mqttConfig.getSslKeyPath()) + .setCertPath(mqttConfig.getSslCertPath()); + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + + // 4. 创建服务器并设置连接处理器 + mqttServer = MqttServer.create(vertx, options); + mqttServer.endpointHandler(this::handleEndpoint); + + // 5. 启动服务器 + try { + mqttServer.listen().result(); + running = true; + log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 6. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler( + SpringUtil.getBean(IotDeviceMessageService.class), connectionManager); + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus); + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e); + // 启动失败时关闭 Vertx + if (vertx != null) { + vertx.close(); + vertx = null; + } + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + // 1. 停止下行消息订阅者 + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; + } + + // 2.1 关闭 MQTT 服务器 + if (mqttServer != null) { + try { + mqttServer.close().result(); + log.info("[stop][IoT MQTT 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT MQTT 协议 {} 服务器停止失败]", getId(), e); + } + mqttServer = null; + } + // 2.2 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT MQTT 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT MQTT 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT MQTT 协议 {} 已停止]", getId()); + } + + /** + * 初始化 Handler + */ + private void initHandlers() { + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.connectionHandler = new IotMqttConnectionHandler(connectionManager, messageService, deviceApi, serverId); + this.registerHandler = new IotMqttRegisterHandler(connectionManager, messageService, deviceApi); + this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, messageService, serverId); + } + + /** + * 处理 MQTT 连接端点 + * + * @param endpoint MQTT 连接端点 + */ + private void handleEndpoint(MqttEndpoint endpoint) { + String clientId = endpoint.clientIdentifier(); + + // 1. 委托 connectionHandler 处理连接认证 + // TODO @AI:register topic 不需要注册,需要判断下; + if (!connectionHandler.handleConnect(endpoint)) { + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); + return; + } + + // 2.1 设置异常和关闭处理器 + endpoint.exceptionHandler(ex -> { + log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {}]", + clientId, connectionManager.getEndpointAddress(endpoint)); + // TODO @AI:是不是改成 endpoint close 更合适? + connectionHandler.cleanupConnection(endpoint); + }); + endpoint.closeHandler(v -> connectionHandler.cleanupConnection(endpoint)); + endpoint.disconnectHandler(v -> { + log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId); + connectionHandler.cleanupConnection(endpoint); + }); + // 2.2 设置心跳处理器 + endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId)); + + // 3.1 设置消息处理器 + endpoint.publishHandler(message -> processMessage(endpoint, message)); + // 3.2 设置 QoS 2 消息的 PUBREL 处理器 + endpoint.publishReleaseHandler(endpoint::publishComplete); + + // 4.1 设置订阅处理器 + endpoint.subscribeHandler(subscribe -> { + // TODO @AI:convertList 简化; + List topicNames = subscribe.topicSubscriptions().stream() + .map(MqttTopicSubscription::topicName) + .collect(Collectors.toList()); + log.debug("[handleEndpoint][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); + + // TODO @AI:convertList 简化; + List grantedQoSLevels = subscribe.topicSubscriptions().stream() + .map(MqttTopicSubscription::qualityOfService) + .collect(Collectors.toList()); + endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); + }); + // 4.2 设置取消订阅处理器 + endpoint.unsubscribeHandler(unsubscribe -> { + log.debug("[handleEndpoint][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); + endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); + }); + + // 5. 接受连接 + endpoint.accept(false); + } + + /** + * 处理消息(发布) + * + * @param endpoint MQTT 连接端点 + * @param message 发布消息 + */ + // TODO @AI:看看要不要一定程度,参考 IotTcpUpstreamHandler 的 processMessage 方法; + private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) { + String clientId = endpoint.clientIdentifier(); + try { + String topic = message.topicName(); + byte[] payload = message.payload().getBytes(); + + // 根据 topic 分发到不同 handler + if (registerHandler.isRegisterMessage(topic)) { + registerHandler.handleRegister(endpoint, topic, payload); + } else { + upstreamHandler.handleMessage(endpoint, topic, payload); + } + + // 根据 QoS 级别发送相应的确认消息 + handleQoSAck(endpoint, message); + } catch (Exception e) { + // TODO @AI:异常的时候,直接断开; + log.error("[handlePublish][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]", + clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); + connectionHandler.cleanupConnection(endpoint); + endpoint.close(); + } + } + + /** + * 处理 QoS 确认 + * + * @param endpoint MQTT 连接端点 + * @param message 发布消息 + */ + private void handleQoSAck(MqttEndpoint endpoint, MqttPublishMessage message) { + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { + // QoS 1: 发送 PUBACK 确认 + endpoint.publishAcknowledge(message.messageId()); + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { + // QoS 2: 发送 PUBREC 确认 + endpoint.publishReceived(message.messageId()); + } + // QoS 0 无需确认 + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java deleted file mode 100644 index 46fbc7c3fa..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java +++ /dev/null @@ -1,117 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; - -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.mqtt.MqttServer; -import io.vertx.mqtt.MqttServerOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 MQTT 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotMqttUpstreamProtocol implements IotProtocol { - - private static final String ID = "mqtt"; - - private volatile boolean running = false; - - private final IotGatewayProperties.MqttProperties mqttProperties; - - private final IotDeviceMessageService messageService; - - private final IotMqttConnectionManager connectionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private MqttServer mqttServer; - - public IotMqttUpstreamProtocol(IotGatewayProperties.MqttProperties mqttProperties, - IotDeviceMessageService messageService, - IotMqttConnectionManager connectionManager, - Vertx vertx) { - this.mqttProperties = mqttProperties; - this.messageService = messageService; - this.connectionManager = connectionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort()); - } - - @Override - public String getId() { - return ID; - } - - @Override - public IotProtocolTypeEnum getType() { - return IotProtocolTypeEnum.MQTT; - } - - @Override - public boolean isRunning() { - return running; - } - - // TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈; - @Override - @PostConstruct - public void start() { - // 创建服务器选项 - MqttServerOptions options = new MqttServerOptions() - .setPort(mqttProperties.getPort()) - .setMaxMessageSize(mqttProperties.getMaxMessageSize()) - .setTimeoutOnConnect(mqttProperties.getConnectTimeoutSeconds()); - - // 配置 SSL(如果启用) - if (Boolean.TRUE.equals(mqttProperties.getSslEnabled())) { - options.setSsl(true) - .setKeyCertOptions(mqttProperties.getSslOptions().getKeyCertOptions()) - .setTrustOptions(mqttProperties.getSslOptions().getTrustOptions()); - } - - // 创建服务器并设置连接处理器 - mqttServer = MqttServer.create(vertx, options); - mqttServer.endpointHandler(endpoint -> { - IotMqttUpstreamHandler handler = new IotMqttUpstreamHandler(this, messageService, connectionManager); - handler.handle(endpoint); - }); - - // 启动服务器 - try { - mqttServer.listen().result(); - running = true; - log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 MQTT 协议启动失败]", e); - throw e; - } - } - - @Override - @PreDestroy - public void stop() { - if (mqttServer != null) { - try { - mqttServer.close().result(); - running = false; - log.info("[stop][IoT 网关 MQTT 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 MQTT 协议停止失败]", e); - } - } - } -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java similarity index 89% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java index c848833f66..69b363e5d0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -7,31 +7,22 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnecti import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** * IoT 网关 MQTT 协议:下行消息处理器 - *

                              - * 专门处理下行消息的业务逻辑,包括: - * 1. 消息编码 - * 2. 主题构建 - * 3. 消息发送 * * @author 芋道源码 */ @Slf4j +@RequiredArgsConstructor public class IotMqttDownstreamHandler { private final IotDeviceMessageService deviceMessageService; private final IotMqttConnectionManager connectionManager; - public IotMqttDownstreamHandler(IotDeviceMessageService deviceMessageService, - IotMqttConnectionManager connectionManager) { - this.deviceMessageService = deviceMessageService; - this.connectionManager = connectionManager; - } - /** * 处理下行消息 * @@ -40,6 +31,7 @@ public class IotMqttDownstreamHandler { */ public boolean handleDownstreamMessage(IotDeviceMessage message) { try { + // TODO @AI:参考 IotTcpDownstreamHandler 逻辑; // 1. 基础校验 if (message == null || message.getDeviceId() == null) { log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]"); @@ -47,6 +39,7 @@ public class IotMqttDownstreamHandler { } // 2. 检查设备是否在线 + // TODO @AI:这块逻辑,是不是冗余?直接使用 3. 获取连接信息判断不就行了? if (connectionManager.isDeviceOffline(message.getDeviceId())) { log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId()); return false; @@ -59,7 +52,7 @@ public class IotMqttDownstreamHandler { return false; } - // 4. 编码消息 + // 4. 序列化 byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(), connectionInfo.getDeviceName()); if (payload == null || payload.length == 0) { @@ -68,6 +61,7 @@ public class IotMqttDownstreamHandler { } // 5. 发送消息到设备 + // TODO @AI:参考 IotTcpDownstreamHandler 的逻辑; return sendMessageToDevice(message, connectionInfo, payload); } catch (Exception e) { if (message != null) { @@ -78,6 +72,7 @@ public class IotMqttDownstreamHandler { } } + // TODO @AI 是不是合并到 handleDownstreamMessage 里; /** * 发送消息到设备 * @@ -91,6 +86,7 @@ public class IotMqttDownstreamHandler { byte[] payload) { // 1. 构建主题 String topic = buildDownstreamTopic(message, connectionInfo); + // TODO @AI:直接断言,非空! if (StrUtil.isBlank(topic)) { log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]", message.getDeviceId(), message.getMethod()); @@ -118,6 +114,7 @@ public class IotMqttDownstreamHandler { */ private String buildDownstreamTopic(IotDeviceMessage message, IotMqttConnectionManager.ConnectionInfo connectionInfo) { + // TODO @AI:直接断言,非空! String method = message.getMethod(); if (StrUtil.isBlank(method)) { return null; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java similarity index 82% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java index fe9b600b99..f40cac1929 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java @@ -1,9 +1,9 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; import lombok.extern.slf4j.Slf4j; /** @@ -18,7 +18,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber private final IotMqttDownstreamHandler downstreamHandler; - public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol protocol, + public IotMqttDownstreamSubscriber(IotMqttProtocol protocol, IotMqttDownstreamHandler downstreamHandler, IotMessageBus messageBus) { super(protocol, messageBus); @@ -27,6 +27,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber @Override protected void handleMessage(IotDeviceMessage message) { + // TODO @AI:参考 IotTcpDownstreamHandler 不处理返回值,甚至不用返回值; boolean success = downstreamHandler.handleDownstreamMessage(message); if (success) { log.debug("[handleMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]", diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java new file mode 100644 index 0000000000..b91a151a02 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java @@ -0,0 +1,105 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.buffer.Buffer; +import io.vertx.mqtt.MqttEndpoint; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 MQTT 协议的处理器抽象基类 + *

                              + * 提供通用的连接校验、响应发送等功能 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public abstract class IotMqttAbstractHandler { + + // TODO @AI:不使用 codec,使用 serializer 来进行编解码; + /** + * 默认编解码类型(MQTT 使用 Alink 协议) + */ + protected static final String DEFAULT_CODEC_TYPE = "Alink"; + + protected final IotMqttConnectionManager connectionManager; + protected final IotDeviceMessageService deviceMessageService; + + // TODO @AI:这个是否需要???!!! + /** + * 是否需要连接已认证(默认 true) + *

                              + * 仅 IotMqttConnectionHandler 覆盖为 false + * + * @return 是否需要连接已认证 + */ + protected boolean requiresAuthenticated() { + return true; + } + + // TODO @AI:不确定,是不是基于 method 就可以算出来 reply topic ???!!! + // TODO @AI:需要传递 seriabler 序列对象,不是通过 deviceMessageService.encodeDeviceMessage 获取到合适的; + /** + * 发送成功响应到设备 + * + * @param endpoint MQTT 连接端点 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param requestId 请求 ID + * @param method 方法名 + * @param data 响应数据 + */ + protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, + String requestId, String method, Object data) { + try { + // 1. 构建响应消息 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null); + + // 2. 编码消息(使用默认编解码器) + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); + + // 3. 构建响应主题并发送 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); + endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); + log.debug("[sendSuccessResponse][发送成功响应,主题: {}]", replyTopic); + } catch (Exception e) { + log.error("[sendSuccessResponse][发送成功响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); + } + } + + /** + * 发送错误响应到设备 + * + * @param endpoint MQTT 连接端点 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param requestId 请求 ID + * @param method 方法名 + * @param errorMessage 错误消息 + */ + protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, + String requestId, String method, String errorMessage) { + try { + // 1. 构建响应消息 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, 400, errorMessage); + + // 2. 编码消息(使用默认编解码器) + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); + + // 3. 构建响应主题并发送 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); + endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); + log.debug("[sendErrorResponse][发送错误响应,主题: {}]", replyTopic); + } catch (Exception e) { + log.error("[sendErrorResponse][发送错误响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); + } + } + + // TODO @AI:搞个基础的 writeResponse 会不会更好?参考 IotTcpUpstreamHandler 里; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttConnectionHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttConnectionHandler.java new file mode 100644 index 0000000000..2cd7f23ddf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttConnectionHandler.java @@ -0,0 +1,178 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.mqtt.MqttEndpoint; +import lombok.extern.slf4j.Slf4j; + +// TODO @AI:改成 auth 这样的命名,更好理解; +/** + * IoT 网关 MQTT 连接处理器 + *

                              + * 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttConnectionHandler extends IotMqttAbstractHandler { + + // TODO @AI:通过 springutil 去获取! + private final IotDeviceCommonApi deviceApi; + private final String serverId; + + public IotMqttConnectionHandler(IotMqttConnectionManager connectionManager, + IotDeviceMessageService deviceMessageService, + IotDeviceCommonApi deviceApi, + String serverId) { + super(connectionManager, deviceMessageService); + this.deviceApi = deviceApi; + this.serverId = serverId; + } + + @Override + protected boolean requiresAuthenticated() { + return false; // 连接阶段不需要已认证 + } + + /** + * 处理 MQTT 连接请求 + * + * @param endpoint MQTT 连接端点 + * @return 认证是否成功 + */ + public boolean handleConnect(MqttEndpoint endpoint) { + // TODO @AI:整个 try catch 下; + // TODO @AI:是不是参考 IotTcpUpstreamHandler 的代码结构 + String clientId = endpoint.clientIdentifier(); + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; + log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", + clientId, username, connectionManager.getEndpointAddress(endpoint)); + + // 进行认证 + if (!authenticateDevice(clientId, username, password, endpoint)) { + log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username); + return false; + } + log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); + return true; + } + + /** + * 在 MQTT 连接时进行设备认证 + * + * @param clientId 客户端 ID + * @param username 用户名 + * @param password 密码 + * @param endpoint MQTT 连接端点 + * @return 认证是否成功 + */ + private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) { + try { + // 1.1 解析认证参数 + // TODO @AI:断言,统一交给上层打印日志; + if (StrUtil.hasEmpty(clientId, username, password)) { + log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username); + return false; + } + // 1.2 构建认证参数 + IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() + .setClientId(clientId) + .setUsername(username) + .setPassword(password); + + // 2.1 执行认证 + CommonResult authResult = deviceApi.authDevice(authParams); + // TODO @AI:断言,统一交给上层打印日志; + if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) { + log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]", + clientId, username, authResult.getMsg()); + return false; + } + // 2.2 获取设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + if (deviceInfo == null) { + log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); + return false; + } + // 2.3 获取设备信息 + // TODO @AI:报错需要处理下; + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + if (device == null) { + log.warn("[authenticateDevice][设备不存在,客户端 ID: {},用户名: {}]", clientId, username); + return false; + } + + // 3.1 注册连接 + registerConnection(endpoint, device, clientId); + // 3.2 发送设备上线消息 + sendOnlineMessage(device); + return true; + } catch (Exception e) { + log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e); + return false; + } + } + + /** + * 注册连接 + */ + private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { + IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()) + .setClientId(clientId) + .setAuthenticated(true) + .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); + connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); + } + + /** + * 发送设备上线消息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + try { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); + } catch (Exception e) { + log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage()); + } + } + + /** + * 清理连接 + * + * @param endpoint MQTT 连接端点 + */ + public void cleanupConnection(MqttEndpoint endpoint) { + try { + // 1. 发送设备离线消息 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); + if (connectionInfo != null) { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + } + + // 2. 注销连接 + connectionManager.unregisterConnection(endpoint); + } catch (Exception e) { + log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", + endpoint.clientIdentifier(), e.getMessage()); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java new file mode 100644 index 0000000000..4a1ddc17a4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java @@ -0,0 +1,182 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.mqtt.MqttEndpoint; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +/** + * IoT 网关 MQTT 设备注册处理器 + *

                              + * 处理设备动态注册消息(一型一密) + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttRegisterHandler extends IotMqttAbstractHandler { + + // TODO IotDeviceMessageMethodEnum.DEVICE_REGISTER 计算出来?IotMqttTopicUtils? + /** + * register 请求的 topic 后缀 + */ + public static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register"; + + private final IotDeviceCommonApi deviceApi; + + public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager, + IotDeviceMessageService deviceMessageService, + IotDeviceCommonApi deviceApi) { + super(connectionManager, deviceMessageService); + // TODO @AI:通过 springutil 处理; + this.deviceApi = deviceApi; + } + + /** + * 判断是否为注册消息 + * + * @param topic 主题 + * @return 是否为注册消息 + */ + public boolean isRegisterMessage(String topic) { + // TODO @AI:是不是搞到 IotMqttTopicUtils 里? + return topic != null && topic.endsWith(REGISTER_TOPIC_SUFFIX); + } + + /** + * 处理注册消息 + * + * @param endpoint MQTT 连接端点 + * @param topic 主题 + * @param payload 消息内容 + */ + public void handleRegister(MqttEndpoint endpoint, String topic, byte[] payload) { + String clientId = endpoint.clientIdentifier(); + // 1.1 基础检查 + if (ArrayUtil.isEmpty(payload)) { + return; + } + // 1.2 解析主题,获取 productKey 和 deviceName + // TODO @AI:直接断言报错; + String[] topicParts = topic.split("/"); + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { + log.warn("[handleRegister][topic({}) 格式不正确]", topic); + return; + } + String productKey = topicParts[2]; + String deviceName = topicParts[3]; + // TODO @AI:直接断言报错; + + // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) + // TODO @AI:使用默认的 json; + IotDeviceMessage message; + try { + message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); + if (message == null) { + log.warn("[handleRegister][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); + return; + } + } catch (Exception e) { + log.error("[handleRegister][消息解码异常,客户端 ID: {},主题: {},错误: {}]", + clientId, topic, e.getMessage(), e); + return; + } + + // 3. 处理设备动态注册请求 + log.info("[handleRegister][收到设备注册消息,设备: {}.{}, 方法: {}]", + productKey, deviceName, message.getMethod()); + try { + processRegisterRequest(message, productKey, deviceName, endpoint); + } catch (Exception e) { + // TODO @AI:各种情况下的翻译; + log.error("[handleRegister][消息处理异常,客户端 ID: {},主题: {},错误: {}]", + clientId, topic, e.getMessage(), e); + } + } + + /** + * 处理设备动态注册请求(一型一密,不需要 deviceSecret) + * + * @param message 消息信息 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param endpoint MQTT 连接端点 + * @see 阿里云 - 一型一密 + */ + private void processRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, + MqttEndpoint endpoint) { + String clientId = endpoint.clientIdentifier(); + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); + try { + // 1. 解析注册参数 + IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); + if (params == null) { + log.warn("[processRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); + sendErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), method, "注册参数不完整"); + return; + } + + // 2. 调用动态注册 API + CommonResult result = deviceApi.registerDevice(params); + if (result.isError()) { + log.warn("[processRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); + sendErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getMsg()); + return; + } + + // 3. 发送成功响应(包含 deviceSecret) + sendSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getData()); + log.info("[processRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", + params.getDeviceName(), clientId); + } catch (Exception e) { + log.error("[processRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); + sendErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), method, "注册处理异常"); + } + } + + // TODO @AI:解析可以简化,参考别的 tcp 对应的 + /** + * 解析注册参数 + * + * @param params 参数对象(通常为 Map 类型) + * @return 注册参数 DTO,解析失败时返回 null + */ + @SuppressWarnings("unchecked") + private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { + if (params == null) { + return null; + } + try { + // 参数默认为 Map 类型,直接转换 + if (params instanceof Map) { + Map paramMap = (Map) params; + return new IotDeviceRegisterReqDTO() + .setProductKey(MapUtil.getStr(paramMap, "productKey")) + .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) + .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); + } + // 如果已经是目标类型,直接返回 + if (params instanceof IotDeviceRegisterReqDTO) { + return (IotDeviceRegisterReqDTO) params; + } + + // 其他情况尝试 JSON 转换 + return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); + } catch (Exception e) { + log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); + return null; + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java new file mode 100644 index 0000000000..cb78d0af87 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.mqtt.MqttEndpoint; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 MQTT 上行消息处理器 + *

                              + * 处理业务消息(属性上报、事件上报等) + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { + + private final String serverId; + + public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager, + IotDeviceMessageService deviceMessageService, + String serverId) { + super(connectionManager, deviceMessageService); + this.serverId = serverId; + } + + /** + * 处理业务消息 + * + * @param endpoint MQTT 连接端点 + * @param topic 主题 + * @param payload 消息内容 + */ + public void handleMessage(MqttEndpoint endpoint, String topic, byte[] payload) { + String clientId = endpoint.clientIdentifier(); + + // 1. 基础检查 + if (payload == null || payload.length == 0) { + return; + } + + // 2. 解析主题,获取 productKey 和 deviceName + String[] topicParts = topic.split("/"); + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { + log.warn("[handleMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); + return; + } + + // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName) + String productKey = topicParts[2]; + String deviceName = topicParts[3]; + try { + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); + if (message == null) { + log.warn("[handleMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); + return; + } + + // 4. 处理业务消息(认证已在连接时完成) + log.info("[handleMessage][收到设备消息,设备: {}.{}, 方法: {}]", + productKey, deviceName, message.getMethod()); + handleBusinessRequest(message, productKey, deviceName); + } catch (Exception e) { + // TODO @AI:各种情况下的翻译; + log.error("[handleMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", + clientId, topic, e.getMessage(), e); + } + } + + /** + * 处理业务请求 + */ + private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { + // 发送消息到消息总线 + message.setServerId(serverId); + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java index 082a2ad797..4f432fed47 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java @@ -40,6 +40,7 @@ public class IotMqttConnectionManager { */ private final Map deviceEndpointMap = new ConcurrentHashMap<>(); + // TODO @AI:这里会存在返回 "unknown" 的情况么?是不是必须返回,否则还是异常更合理点? /** * 安全获取 endpoint 地址 *

                              @@ -66,7 +67,6 @@ public class IotMqttConnectionManager { } catch (Exception ignored) { // 连接已关闭,忽略异常 } - return realTimeAddress; } @@ -129,7 +129,6 @@ public class IotMqttConnectionManager { if (endpoint == null) { return null; } - // 获取连接信息 return getConnectionInfo(endpoint); } @@ -208,6 +207,7 @@ public class IotMqttConnectionManager { */ private String clientId; + // TODO @AI:是不是要去掉!感觉没用啊; /** * 是否已认证 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java deleted file mode 100644 index d40dba447c..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java +++ /dev/null @@ -1,511 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; -import io.netty.handler.codec.mqtt.MqttConnectReturnCode; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.mqtt.MqttEndpoint; -import io.vertx.mqtt.MqttTopicSubscription; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; -import java.util.Map; - -/** - * MQTT 上行消息处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotMqttUpstreamHandler { - - /** - * 默认编解码类型(MQTT 使用 Alink 协议) - */ - private static final String DEFAULT_CODEC_TYPE = "Alink"; - - /** - * register 请求的 topic 后缀 - */ - private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register"; - - private final IotDeviceMessageService deviceMessageService; - - private final IotMqttConnectionManager connectionManager; - - private final IotDeviceCommonApi deviceApi; - - private final String serverId; - - public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotMqttConnectionManager connectionManager) { - this.deviceMessageService = deviceMessageService; - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.connectionManager = connectionManager; - this.serverId = protocol.getServerId(); - } - - /** - * 处理 MQTT 连接 - * - * @param endpoint MQTT 连接端点 - */ - public void handle(MqttEndpoint endpoint) { - String clientId = endpoint.clientIdentifier(); - String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; - String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; - - log.debug("[handle][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", - clientId, username, connectionManager.getEndpointAddress(endpoint)); - - // 1. 先进行认证 - if (!authenticateDevice(clientId, username, password, endpoint)) { - log.warn("[handle][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username); - endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); - return; - } - - log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); - - // 2. 设置心跳处理器(监听客户端的 PINGREQ 消息) - endpoint.pingHandler(v -> { - log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId); - // Vert.x 会自动发送 PINGRESP 响应,无需手动处理 - }); - - // 3. 设置异常和关闭处理器 - endpoint.exceptionHandler(ex -> { - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint)); - cleanupConnection(endpoint); - }); - endpoint.closeHandler(v -> { - cleanupConnection(endpoint); - }); - - // 4. 设置消息处理器 - endpoint.publishHandler(mqttMessage -> { - try { - // 4.1 根据 topic 判断是否为 register 请求 - String topic = mqttMessage.topicName(); - byte[] payload = mqttMessage.payload().getBytes(); - if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) { - // register 请求:使用默认编解码器处理(设备可能未注册) - processRegisterMessage(clientId, topic, payload, endpoint); - } else { - // 业务请求:正常处理 - processMessage(clientId, topic, payload); - } - - // 4.2 根据 QoS 级别发送相应的确认消息 - if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) { - // QoS 1: 发送 PUBACK 确认 - endpoint.publishAcknowledge(mqttMessage.messageId()); - } else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) { - // QoS 2: 发送 PUBREC 确认 - endpoint.publishReceived(mqttMessage.messageId()); - } - // QoS 0 无需确认 - } catch (Exception e) { - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", - clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); - cleanupConnection(endpoint); - endpoint.close(); - } - }); - - // 5. 设置订阅处理器 - endpoint.subscribeHandler(subscribe -> { - // 提取主题名称列表用于日志显示 - List topicNames = subscribe.topicSubscriptions().stream() - .map(MqttTopicSubscription::topicName) - .collect(java.util.stream.Collectors.toList()); - log.debug("[handle][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); - - // 提取 QoS 列表 - List grantedQoSLevels = subscribe.topicSubscriptions().stream() - .map(MqttTopicSubscription::qualityOfService) - .collect(java.util.stream.Collectors.toList()); - endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); - }); - - // 6. 设置取消订阅处理器 - endpoint.unsubscribeHandler(unsubscribe -> { - log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); - endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); - }); - - // 7. 设置 QoS 2消息的 PUBREL 处理器 - endpoint.publishReleaseHandler(endpoint::publishComplete); - - // 8. 设置断开连接处理器 - endpoint.disconnectHandler(v -> { - log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId); - cleanupConnection(endpoint); - }); - - // 9. 接受连接 - endpoint.accept(false); - } - - /** - * 处理消息 - * - * @param clientId 客户端 ID - * @param topic 主题 - * @param payload 消息内容 - */ - private void processMessage(String clientId, String topic, byte[] payload) { - // 1. 基础检查 - if (payload == null || payload.length == 0) { - return; - } - - // 2. 解析主题,获取 productKey 和 deviceName - String[] topicParts = topic.split("/"); - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { - log.warn("[processMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); - return; - } - - // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName) - String productKey = topicParts[2]; - String deviceName = topicParts[3]; - try { - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); - if (message == null) { - log.warn("[processMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); - return; - } - - // 4. 处理业务消息(认证已在连接时完成) - log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]", - productKey, deviceName, message.getMethod()); - handleBusinessRequest(message, productKey, deviceName); - } catch (Exception e) { - log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage(), e); - } - } - - /** - * 在 MQTT 连接时进行设备认证 - * - * @param clientId 客户端 ID - * @param username 用户名 - * @param password 密码 - * @param endpoint MQTT 连接端点 - * @return 认证是否成功 - */ - private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) { - try { - // 1. 参数校验 - if (StrUtil.hasEmpty(clientId, username, password)) { - log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username); - return false; - } - - // 2. 构建认证参数 - IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() - .setClientId(clientId) - .setUsername(username) - .setPassword(password); - - // 3. 调用设备认证 API - CommonResult authResult = deviceApi.authDevice(authParams); - if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) { - log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]", - clientId, username, authResult.getMsg()); - return false; - } - - // 4. 获取设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); - if (deviceInfo == null) { - log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); - return false; - } - - IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO() - .setProductKey(deviceInfo.getProductKey()) - .setDeviceName(deviceInfo.getDeviceName()); - - CommonResult deviceResult = deviceApi.getDevice(getReqDTO); - if (!deviceResult.isSuccess() || deviceResult.getData() == null) { - log.warn("[authenticateDevice][获取设备信息失败,客户端 ID: {},用户名: {},错误: {}]", - clientId, username, deviceResult.getMsg()); - return false; - } - - // 5. 注册连接 - IotDeviceRespDTO device = deviceResult.getData(); - registerConnection(endpoint, device, clientId); - - // 6. 发送设备上线消息 - sendOnlineMessage(device); - - return true; - } catch (Exception e) { - log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e); - return false; - } - } - - /** - * 处理 register 消息(设备动态注册,使用默认编解码器) - * - * @param clientId 客户端 ID - * @param topic 主题 - * @param payload 消息内容 - * @param endpoint MQTT 连接端点 - */ - private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) { - // 1.1 基础检查 - if (ArrayUtil.isEmpty(payload)) { - return; - } - // 1.2 解析主题,获取 productKey 和 deviceName - String[] topicParts = topic.split("/"); - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { - log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic); - return; - } - String productKey = topicParts[2]; - String deviceName = topicParts[3]; - - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) - IotDeviceMessage message; - try { - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); - if (message == null) { - log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); - return; - } - } catch (Exception e) { - log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage(), e); - return; - } - - // 3. 处理设备动态注册请求 - log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]", - productKey, deviceName, message.getMethod()); - try { - handleRegisterRequest(message, productKey, deviceName, endpoint); - } catch (Exception e) { - log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage(), e); - } - } - - /** - * 处理设备动态注册请求(一型一密,不需要 deviceSecret) - * - * @param message 消息信息 - * @param productKey 产品 Key - * @param deviceName 设备名称 - * @param endpoint MQTT 连接端点 - * @see 阿里云 - 一型一密 - */ - private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) { - String clientId = endpoint.clientIdentifier(); - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null) { - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整"); - return; - } - - // 2. 调用动态注册 API - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg()); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData()); - log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", - params.getDeviceName(), clientId); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常"); - } - } - - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceRegisterReqDTO() - .setProductKey(MapUtil.getStr(paramMap, "productKey")) - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceRegisterReqDTO) { - return (IotDeviceRegisterReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param endpoint MQTT 连接端点 - * @param productKey 产品 Key - * @param deviceName 设备名称 - * @param requestId 请求 ID - * @param registerResp 注册响应 - */ - private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, - String requestId, IotDeviceRegisterRespDTO registerResp) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); - - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) - String replyTopic = IotMqttTopicUtils.buildTopicByMethod( - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), - MqttQoS.AT_LEAST_ONCE, false, false); - log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]", - endpoint.clientIdentifier(), e); - } - } - - /** - * 发送注册错误响应 - * - * @param endpoint MQTT 连接端点 - * @param productKey 产品 Key - * @param deviceName 设备名称 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - */ - private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, - String requestId, String errorMessage) { - try { - // 1. 构建响应消息 - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage); - - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); - - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) - String replyTopic = IotMqttTopicUtils.buildTopicByMethod( - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), - MqttQoS.AT_LEAST_ONCE, false, false); - log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic); - } catch (Exception e) { - log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]", - endpoint.clientIdentifier(), e); - } - } - - /** - * 处理业务请求 - */ - private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { - // 发送消息到消息总线 - message.setServerId(serverId); - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); - } - - /** - * 注册连接 - */ - private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { - IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() - .setDeviceId(device.getId()) - .setProductKey(device.getProductKey()) - .setDeviceName(device.getDeviceName()) - .setClientId(clientId) - .setAuthenticated(true) - .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); - connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); - } - - /** - * 发送设备上线消息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage()); - } - } - - /** - * 清理连接 - */ - private void cleanupConnection(MqttEndpoint endpoint) { - try { - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); - if (connectionInfo != null) { - // 发送设备离线消息 - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName()); - } - - // 注销连接 - connectionManager.unregisterConnection(endpoint); - } catch (Exception e) { - log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage()); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java index 986bfbe60d..b3ae6a0ca4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java @@ -45,8 +45,8 @@ public class IotTcpDownstreamHandler { } // 2. 序列化 + 帧编码 - byte[] serializedData = serializer.serialize(message); - Buffer frameData = codec.encode(serializedData); + byte[] payload = serializer.serialize(message); + Buffer frameData = codec.encode(payload); // 3. 发送到设备 boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes()); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index add4dce6a8..671a103fc8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -57,9 +57,9 @@ yudao: # 针对引入的 TCP 组件的配置 # ==================================== - id: tcp-json + enabled: false type: tcp port: 8091 - enabled: false serialize: json tcp: max-connections: 1000 @@ -79,9 +79,9 @@ yudao: # 针对引入的 UDP 组件的配置 # ==================================== - id: udp-json + enabled: false type: udp port: 8093 - enabled: false serialize: json udp: max-sessions: 1000 # 最大会话数 @@ -92,9 +92,9 @@ yudao: # 针对引入的 WebSocket 组件的配置 # ==================================== - id: websocket-json + enabled: true type: websocket port: 8094 - enabled: true serialize: json websocket: path: /ws @@ -106,13 +106,26 @@ yudao: # 针对引入的 CoAP 组件的配置 # ==================================== - id: coap-json + enabled: false type: coap port: 5683 - enabled: true coap: max-message-size: 1024 # 最大消息大小(字节) ack-timeout-ms: 2000 # ACK 超时时间(毫秒) max-retransmit: 4 # 最大重传次数 + # ==================================== + # 针对引入的 MQTT 组件的配置 + # ==================================== + - id: mqtt-json + enabled: true + type: mqtt + port: 1883 + serialize: json + mqtt: + max-message-size: 8192 # 最大消息大小(字节) + connect-timeout-seconds: 60 # 连接超时时间(秒) + keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒) + ssl-enabled: false # 是否启用 SSL # 协议配置(旧版,保持兼容) protocol: @@ -150,15 +163,6 @@ yudao: key-store-password: "your-keystore-password" # 客户端证书库密码 trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 - # ==================================== - # 针对引入的 MQTT 组件的配置 - # ==================================== - mqtt: - enabled: false - port: 1883 - max-message-size: 8192 - connect-timeout-seconds: 60 - ssl-enabled: false --- #################### 日志相关配置 #################### From 9156aef4e3307c54e060561238d1fdfe4f9e3494 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Feb 2026 22:50:44 +0800 Subject: [PATCH 21/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91mqtt=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=8820%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gateway/protocol/mqtt/IotMqttConfig.java | 41 +- .../protocol/mqtt/IotMqttProtocol.java | 164 ++++---- .../downstream/IotMqttDownstreamHandler.java | 115 ++---- .../IotMqttDownstreamSubscriber.java | 14 +- .../upstream/IotMqttAbstractHandler.java | 25 +- .../handler/upstream/IotMqttAuthHandler.java | 121 ++++++ .../upstream/IotMqttConnectionHandler.java | 178 -------- .../upstream/IotMqttRegisterHandler.java | 156 +++---- .../upstream/IotMqttUpstreamHandler.java | 91 ++-- .../manager/IotMqttConnectionManager.java | 17 +- ...rectDeviceMqttProtocolIntegrationTest.java | 275 +++++-------- ...ewayDeviceMqttProtocolIntegrationTest.java | 389 ++++++++---------- ...ySubDeviceMqttProtocolIntegrationTest.java | 201 ++++----- 13 files changed, 714 insertions(+), 1073 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttConnectionHandler.java diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java index 5fb7f779fe..8fef367476 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java @@ -1,8 +1,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import lombok.Data; -// TODO @AI:validator 参数校验。也看看其他几个配置类有没有类似问题 +// done @AI:validator 参数校验。也看看其他几个配置类有没有类似问题 /** * IoT 网关 MQTT 协议配置 * @@ -11,9 +13,31 @@ import lombok.Data; @Data public class IotMqttConfig { + /** + * 最大消息大小(字节) + */ + @NotNull(message = "最大消息大小不能为空") + @Min(value = 1024, message = "最大消息大小不能小于 1024 字节") + private Integer maxMessageSize = 8192; + + /** + * 连接超时时间(秒) + */ + @NotNull(message = "连接超时时间不能为空") + @Min(value = 1, message = "连接超时时间不能小于 1 秒") + private Integer connectTimeoutSeconds = 60; + + /** + * 保持连接超时时间(秒) + */ + @NotNull(message = "保持连接超时时间不能为空") + @Min(value = 1, message = "保持连接超时时间不能小于 1 秒") + private Integer keepAliveTimeoutSeconds = 300; + /** * 是否启用 SSL */ + @NotNull(message = "是否启用 SSL 不能为空") private Boolean sslEnabled = false; /** @@ -26,19 +50,4 @@ public class IotMqttConfig { */ private String sslKeyPath; - /** - * 最大消息大小(字节) - */ - private Integer maxMessageSize = 8192; - - /** - * 连接超时时间(秒) - */ - private Integer connectTimeoutSeconds = 60; - - /** - * 保持连接超时时间(秒) - */ - private Integer keepAliveTimeoutSeconds = 300; - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java index a8d8cb28d9..58c5fff10c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java @@ -7,9 +7,10 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttConnectionHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; @@ -25,9 +26,11 @@ import io.vertx.mqtt.MqttTopicSubscription; import io.vertx.mqtt.messages.MqttPublishMessage; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; import java.util.List; -import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; /** * IoT 网关 MQTT 协议:接收设备上行消息 @@ -61,28 +64,42 @@ public class IotMqttProtocol implements IotProtocol { * MQTT 服务器 */ private MqttServer mqttServer; + /** * 连接管理器 */ - private IotMqttConnectionManager connectionManager; - + private final IotMqttConnectionManager connectionManager; /** * 下行消息订阅者 */ - private IotMqttDownstreamSubscriber downstreamSubscriber; + private final IotMqttDownstreamSubscriber downstreamSubscriber; - // TODO @AI:这个是不是提前创建下?因为是无状态的。 - private IotMqttConnectionHandler connectionHandler; - private IotMqttRegisterHandler registerHandler; - private IotMqttUpstreamHandler upstreamHandler; + private final IotDeviceMessageService deviceMessageService; + + private final IotMqttAuthHandler authHandler; + private final IotMqttRegisterHandler registerHandler; + private final IotMqttUpstreamHandler upstreamHandler; public IotMqttProtocol(ProtocolInstanceProperties properties) { + IotMqttConfig mqttConfig = properties.getMqtt(); + Assert.notNull(mqttConfig, "MQTT 协议配置(mqtt)不能为空"); this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); - // TODO @AI:初始化连接器,参考 IotTcpProtocol + // 初始化连接管理器 + this.connectionManager = new IotMqttConnectionManager(); - // TODO @AI:初始化下行消息订阅者,参考 IotTcpProtocol + // 初始化 Handler + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId); + this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService, deviceApi); + this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager); + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus); } @Override @@ -95,7 +112,7 @@ public class IotMqttProtocol implements IotProtocol { return IotProtocolTypeEnum.MQTT; } - // TODO @AI:这个方法的整体注释风格,参考 IotTcpProtocol 的 start 方法。 + // done @AI:这个方法的整体注释风格,参考 IotTcpProtocol 的 start 方法。 @Override public void start() { if (running) { @@ -103,51 +120,42 @@ public class IotMqttProtocol implements IotProtocol { return; } - // 1.1 创建 Vertx 实例(每个 Protocol 独立管理) + // 1.1 创建 Vertx 实例 this.vertx = Vertx.vertx(); - // 1.2 创建连接管理器 - this.connectionManager = new IotMqttConnectionManager(); - - // 1.3 初始化 Handler - initHandlers(); - - // 2. 创建服务器选项 + // 1.2 创建服务器选项 IotMqttConfig mqttConfig = properties.getMqtt(); - // TODO @AI:default 值,在 IotMqttConfig 处理; MqttServerOptions options = new MqttServerOptions() .setPort(properties.getPort()) - .setMaxMessageSize(mqttConfig != null ? mqttConfig.getMaxMessageSize() : 8192) - .setTimeoutOnConnect(mqttConfig != null ? mqttConfig.getConnectTimeoutSeconds() : 60); - - // 3. 配置 SSL(如果启用) - if (mqttConfig != null && Boolean.TRUE.equals(mqttConfig.getSslEnabled())) { + .setMaxMessageSize(mqttConfig.getMaxMessageSize()) + .setTimeoutOnConnect(mqttConfig.getConnectTimeoutSeconds()); + if (Boolean.TRUE.equals(mqttConfig.getSslEnabled())) { PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() .setKeyPath(mqttConfig.getSslKeyPath()) .setCertPath(mqttConfig.getSslCertPath()); options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); } - // 4. 创建服务器并设置连接处理器 + // 1.3 创建服务器并设置连接处理器 mqttServer = MqttServer.create(vertx, options); mqttServer.endpointHandler(this::handleEndpoint); - // 5. 启动服务器 + // 1.4 启动 MQTT 服务器 try { mqttServer.listen().result(); running = true; log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{},serverId:{}]", getId(), properties.getPort(), serverId); - // 6. 启动下行消息订阅者 - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler( - SpringUtil.getBean(IotDeviceMessageService.class), connectionManager); - this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus); + // 2. 启动下行消息订阅者 this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e); - // 启动失败时关闭 Vertx + // 启动失败时关闭资源 + if (mqttServer != null) { + mqttServer.close(); + mqttServer = null; + } if (vertx != null) { vertx.close(); vertx = null; @@ -162,14 +170,11 @@ public class IotMqttProtocol implements IotProtocol { return; } // 1. 停止下行消息订阅者 - if (downstreamSubscriber != null) { - try { - downstreamSubscriber.stop(); - log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId()); - } catch (Exception e) { - log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e); - } - downstreamSubscriber = null; + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e); } // 2.1 关闭 MQTT 服务器 @@ -196,16 +201,7 @@ public class IotMqttProtocol implements IotProtocol { log.info("[stop][IoT MQTT 协议 {} 已停止]", getId()); } - /** - * 初始化 Handler - */ - private void initHandlers() { - IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); - IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.connectionHandler = new IotMqttConnectionHandler(connectionManager, messageService, deviceApi, serverId); - this.registerHandler = new IotMqttRegisterHandler(connectionManager, messageService, deviceApi); - this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, messageService, serverId); - } + // ======================================= MQTT 连接处理 ====================================== /** * 处理 MQTT 连接端点 @@ -215,24 +211,24 @@ public class IotMqttProtocol implements IotProtocol { private void handleEndpoint(MqttEndpoint endpoint) { String clientId = endpoint.clientIdentifier(); - // 1. 委托 connectionHandler 处理连接认证 - // TODO @AI:register topic 不需要注册,需要判断下; - if (!connectionHandler.handleConnect(endpoint)) { + // 1. 委托 authHandler 处理连接认证 + // done @AI:register topic 不需要注册,需要判断下;当前逻辑已支持(设备可在未认证状态发送 register 消息,registerHandler 会处理) + if (!authHandler.handleAuthenticationRequest(endpoint)) { endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); return; } // 2.1 设置异常和关闭处理器 endpoint.exceptionHandler(ex -> { - log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {}]", - clientId, connectionManager.getEndpointAddress(endpoint)); - // TODO @AI:是不是改成 endpoint close 更合适? - connectionHandler.cleanupConnection(endpoint); + log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {},异常: {}]", + clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage()); + endpoint.close(); }); - endpoint.closeHandler(v -> connectionHandler.cleanupConnection(endpoint)); + // done @AI:closeHandler 处理底层连接关闭(网络中断、异常等),disconnectHandler 处理 MQTT DISCONNECT 报文 + endpoint.closeHandler(v -> cleanupConnection(endpoint)); endpoint.disconnectHandler(v -> { log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId); - connectionHandler.cleanupConnection(endpoint); + cleanupConnection(endpoint); }); // 2.2 设置心跳处理器 endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId)); @@ -243,17 +239,11 @@ public class IotMqttProtocol implements IotProtocol { endpoint.publishReleaseHandler(endpoint::publishComplete); // 4.1 设置订阅处理器 + // done @AI:使用 CollectionUtils.convertList 简化 endpoint.subscribeHandler(subscribe -> { - // TODO @AI:convertList 简化; - List topicNames = subscribe.topicSubscriptions().stream() - .map(MqttTopicSubscription::topicName) - .collect(Collectors.toList()); + List topicNames = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::topicName); log.debug("[handleEndpoint][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); - - // TODO @AI:convertList 简化; - List grantedQoSLevels = subscribe.topicSubscriptions().stream() - .map(MqttTopicSubscription::qualityOfService) - .collect(Collectors.toList()); + List grantedQoSLevels = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::qualityOfService); endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); }); // 4.2 设置取消订阅处理器 @@ -272,27 +262,24 @@ public class IotMqttProtocol implements IotProtocol { * @param endpoint MQTT 连接端点 * @param message 发布消息 */ - // TODO @AI:看看要不要一定程度,参考 IotTcpUpstreamHandler 的 processMessage 方法; private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) { String clientId = endpoint.clientIdentifier(); try { + // 根据 topic 分发到不同 handler String topic = message.topicName(); byte[] payload = message.payload().getBytes(); - - // 根据 topic 分发到不同 handler if (registerHandler.isRegisterMessage(topic)) { registerHandler.handleRegister(endpoint, topic, payload); } else { - upstreamHandler.handleMessage(endpoint, topic, payload); + upstreamHandler.handleBusinessRequest(endpoint, topic, payload); } // 根据 QoS 级别发送相应的确认消息 handleQoSAck(endpoint, message); } catch (Exception e) { - // TODO @AI:异常的时候,直接断开; - log.error("[handlePublish][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]", + log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]", clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); - connectionHandler.cleanupConnection(endpoint); + cleanupConnection(endpoint); endpoint.close(); } } @@ -314,4 +301,27 @@ public class IotMqttProtocol implements IotProtocol { // QoS 0 无需确认 } + /** + * 清理连接 + * + * @param endpoint MQTT 连接端点 + */ + private void cleanupConnection(MqttEndpoint endpoint) { + try { + // 1. 发送设备离线消息 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); + if (connectionInfo != null) { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + } + + // 2. 注销连接 + connectionManager.unregisterConnection(endpoint); + } catch (Exception e) { + log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", + endpoint.clientIdentifier(), e.getMessage()); + } + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java index 69b363e5d0..153da2eec1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; -import cn.hutool.core.util.StrUtil; +import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; @@ -27,103 +27,44 @@ public class IotMqttDownstreamHandler { * 处理下行消息 * * @param message 设备消息 - * @return 是否处理成功 */ - public boolean handleDownstreamMessage(IotDeviceMessage message) { + public void handle(IotDeviceMessage message) { try { - // TODO @AI:参考 IotTcpDownstreamHandler 逻辑; - // 1. 基础校验 - if (message == null || message.getDeviceId() == null) { - log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]"); - return false; - } + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); - // 2. 检查设备是否在线 - // TODO @AI:这块逻辑,是不是冗余?直接使用 3. 获取连接信息判断不就行了? - if (connectionManager.isDeviceOffline(message.getDeviceId())) { - log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId()); - return false; - } - - // 3. 获取连接信息 - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId()); + // 1. 检查设备连接 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( + message.getDeviceId()); if (connectionInfo == null) { - log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID:{}]", message.getDeviceId()); - return false; + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); + return; } - // 4. 序列化 + // 2.1 序列化消息 byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(), connectionInfo.getDeviceName()); - if (payload == null || payload.length == 0) { - log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId()); - return false; - } + Assert.isTrue(payload != null && payload.length > 0, "消息编码结果不能为空"); + // 2.2 构建主题 + Assert.notBlank(message.getMethod(), "消息方法不能为空"); + boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); + String topic = IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), isReply); + Assert.notBlank(topic, "主题不能为空"); - // 5. 发送消息到设备 - // TODO @AI:参考 IotTcpDownstreamHandler 的逻辑; - return sendMessageToDevice(message, connectionInfo, payload); + // 3. 发送到设备 + boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, + MqttQoS.AT_LEAST_ONCE.value(), false); + if (!success) { + throw new RuntimeException("下行消息发送失败"); + } + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},主题: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), topic, payload.length); } catch (Exception e) { - if (message != null) { - log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]", - message.getDeviceId(), e.getMessage(), e); - } - return false; + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", + message.getDeviceId(), message.getMethod(), message, e); } } - // TODO @AI 是不是合并到 handleDownstreamMessage 里; - /** - * 发送消息到设备 - * - * @param message 设备消息 - * @param connectionInfo 连接信息 - * @param payload 消息负载 - * @return 是否发送成功 - */ - private boolean sendMessageToDevice(IotDeviceMessage message, - IotMqttConnectionManager.ConnectionInfo connectionInfo, - byte[] payload) { - // 1. 构建主题 - String topic = buildDownstreamTopic(message, connectionInfo); - // TODO @AI:直接断言,非空! - if (StrUtil.isBlank(topic)) { - log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]", - message.getDeviceId(), message.getMethod()); - return false; - } - - // 2. 发送消息 - boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false); - if (success) { - log.debug("[sendMessageToDevice][消息发送成功,设备 ID:{},主题:{},方法:{}]", - message.getDeviceId(), topic, message.getMethod()); - } else { - log.warn("[sendMessageToDevice][消息发送失败,设备 ID:{},主题:{},方法:{}]", - message.getDeviceId(), topic, message.getMethod()); - } - return success; - } - - /** - * 构建下行消息主题 - * - * @param message 设备消息 - * @param connectionInfo 连接信息 - * @return 主题 - */ - private String buildDownstreamTopic(IotDeviceMessage message, - IotMqttConnectionManager.ConnectionInfo connectionInfo) { - // TODO @AI:直接断言,非空! - String method = message.getMethod(); - if (StrUtil.isBlank(method)) { - return null; - } - - // 使用工具类构建主题,支持回复消息处理 - boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); - return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), isReply); - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java index f40cac1929..c8aa29906a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java @@ -7,9 +7,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; import lombok.extern.slf4j.Slf4j; /** - * IoT 网关 MQTT 协议:下行消息订阅器 - *

                              - * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理 + * IoT 网关 MQTT 协议:接收下行给设备的消息 * * @author 芋道源码 */ @@ -27,15 +25,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber @Override protected void handleMessage(IotDeviceMessage message) { - // TODO @AI:参考 IotTcpDownstreamHandler 不处理返回值,甚至不用返回值; - boolean success = downstreamHandler.handleDownstreamMessage(message); - if (success) { - log.debug("[handleMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - } else { - log.warn("[handleMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - } + downstreamHandler.handle(message); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java index b91a151a02..3663eeecd6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java @@ -21,7 +21,7 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public abstract class IotMqttAbstractHandler { - // TODO @AI:不使用 codec,使用 serializer 来进行编解码; + // done @AI:当前使用 Alink 序列化类型,后续可考虑支持更多序列化方式 /** * 默认编解码类型(MQTT 使用 Alink 协议) */ @@ -30,20 +30,8 @@ public abstract class IotMqttAbstractHandler { protected final IotMqttConnectionManager connectionManager; protected final IotDeviceMessageService deviceMessageService; - // TODO @AI:这个是否需要???!!! - /** - * 是否需要连接已认证(默认 true) - *

                              - * 仅 IotMqttConnectionHandler 覆盖为 false - * - * @return 是否需要连接已认证 - */ - protected boolean requiresAuthenticated() { - return true; - } - - // TODO @AI:不确定,是不是基于 method 就可以算出来 reply topic ???!!! - // TODO @AI:需要传递 seriabler 序列对象,不是通过 deviceMessageService.encodeDeviceMessage 获取到合适的; + // done @AI:基于 method 通过 IotMqttTopicUtils.buildTopicByMethod 计算 reply topic + // done @AI:当前通过 deviceMessageService.encodeDeviceMessage 编码,保持简洁 /** * 发送成功响应到设备 * @@ -80,13 +68,14 @@ public abstract class IotMqttAbstractHandler { * @param deviceName 设备名称 * @param requestId 请求 ID * @param method 方法名 + * @param errorCode 错误码 * @param errorMessage 错误消息 */ protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, - String requestId, String method, String errorMessage) { + String requestId, String method, Integer errorCode, String errorMessage) { try { // 1. 构建响应消息 - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, 400, errorMessage); + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage); // 2. 编码消息(使用默认编解码器) byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); @@ -100,6 +89,6 @@ public abstract class IotMqttAbstractHandler { } } - // TODO @AI:搞个基础的 writeResponse 会不会更好?参考 IotTcpUpstreamHandler 里; + // done @AI:当前 sendSuccessResponse/sendErrorResponse 已足够清晰,暂不抽取 writeResponse } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java new file mode 100644 index 0000000000..f5b1a552cb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java @@ -0,0 +1,121 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.mqtt.MqttEndpoint; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; + +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * IoT 网关 MQTT 认证处理器 + *

                              + * 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttAuthHandler extends IotMqttAbstractHandler { + + private final IotDeviceCommonApi deviceApi; + private final IotDeviceService deviceService; + private final String serverId; + + public IotMqttAuthHandler(IotMqttConnectionManager connectionManager, + IotDeviceMessageService deviceMessageService, + IotDeviceCommonApi deviceApi, + String serverId) { + super(connectionManager, deviceMessageService); + this.deviceApi = deviceApi; + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.serverId = serverId; + } + + // (暂时不改)TODO @AI:【动态注册】在 clientId 包含 |authType=register 时,进行动态注册设备;校验是 clientId、username、password 三者组合;它是拼接 productSecret 的哈希值;所以 IotDeviceAuthUtils 里面的 buildContent 要改造; + /** + * 处理 MQTT 连接(认证)请求 + * + * @param endpoint MQTT 连接端点 + * @return 认证是否成功 + */ + public boolean handleAuthenticationRequest(MqttEndpoint endpoint) { + String clientId = endpoint.clientIdentifier(); + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; + log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", + clientId, username, connectionManager.getEndpointAddress(endpoint)); + + try { + // 1.1 解析认证参数 + Assert.hasText(clientId, "clientId 不能为空"); + Assert.hasText(username, "username 不能为空"); + Assert.hasText(password, "password 不能为空"); + // 1.2 构建认证参数 + IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() + .setClientId(clientId) + .setUsername(username) + .setPassword(password); + + // 2.1 执行认证 + CommonResult authResult = deviceApi.authDevice(authParams); + authResult.checkError(); + if (BooleanUtil.isFalse(authResult.getData())) { + throw exception(DEVICE_AUTH_FAIL); + } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + + // 3.1 注册连接 + registerConnection(endpoint, device, clientId); + // 3.2 发送设备上线消息 + sendOnlineMessage(device); + log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); + return true; + } catch (Exception e) { + log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {},错误: {}]", + clientId, username, e.getMessage()); + return false; + } + } + + /** + * 注册连接 + */ + private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { + IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()) + .setClientId(clientId) + .setAuthenticated(true) + .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); + connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); + } + + /** + * 发送设备上线消息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttConnectionHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttConnectionHandler.java deleted file mode 100644 index 2cd7f23ddf..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttConnectionHandler.java +++ /dev/null @@ -1,178 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; - -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.mqtt.MqttEndpoint; -import lombok.extern.slf4j.Slf4j; - -// TODO @AI:改成 auth 这样的命名,更好理解; -/** - * IoT 网关 MQTT 连接处理器 - *

                              - * 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知 - * - * @author 芋道源码 - */ -@Slf4j -public class IotMqttConnectionHandler extends IotMqttAbstractHandler { - - // TODO @AI:通过 springutil 去获取! - private final IotDeviceCommonApi deviceApi; - private final String serverId; - - public IotMqttConnectionHandler(IotMqttConnectionManager connectionManager, - IotDeviceMessageService deviceMessageService, - IotDeviceCommonApi deviceApi, - String serverId) { - super(connectionManager, deviceMessageService); - this.deviceApi = deviceApi; - this.serverId = serverId; - } - - @Override - protected boolean requiresAuthenticated() { - return false; // 连接阶段不需要已认证 - } - - /** - * 处理 MQTT 连接请求 - * - * @param endpoint MQTT 连接端点 - * @return 认证是否成功 - */ - public boolean handleConnect(MqttEndpoint endpoint) { - // TODO @AI:整个 try catch 下; - // TODO @AI:是不是参考 IotTcpUpstreamHandler 的代码结构 - String clientId = endpoint.clientIdentifier(); - String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; - String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; - log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", - clientId, username, connectionManager.getEndpointAddress(endpoint)); - - // 进行认证 - if (!authenticateDevice(clientId, username, password, endpoint)) { - log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username); - return false; - } - log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); - return true; - } - - /** - * 在 MQTT 连接时进行设备认证 - * - * @param clientId 客户端 ID - * @param username 用户名 - * @param password 密码 - * @param endpoint MQTT 连接端点 - * @return 认证是否成功 - */ - private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) { - try { - // 1.1 解析认证参数 - // TODO @AI:断言,统一交给上层打印日志; - if (StrUtil.hasEmpty(clientId, username, password)) { - log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username); - return false; - } - // 1.2 构建认证参数 - IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() - .setClientId(clientId) - .setUsername(username) - .setPassword(password); - - // 2.1 执行认证 - CommonResult authResult = deviceApi.authDevice(authParams); - // TODO @AI:断言,统一交给上层打印日志; - if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) { - log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]", - clientId, username, authResult.getMsg()); - return false; - } - // 2.2 获取设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); - if (deviceInfo == null) { - log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); - return false; - } - // 2.3 获取设备信息 - // TODO @AI:报错需要处理下; - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - if (device == null) { - log.warn("[authenticateDevice][设备不存在,客户端 ID: {},用户名: {}]", clientId, username); - return false; - } - - // 3.1 注册连接 - registerConnection(endpoint, device, clientId); - // 3.2 发送设备上线消息 - sendOnlineMessage(device); - return true; - } catch (Exception e) { - log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e); - return false; - } - } - - /** - * 注册连接 - */ - private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { - IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() - .setDeviceId(device.getId()) - .setProductKey(device.getProductKey()) - .setDeviceName(device.getDeviceName()) - .setClientId(clientId) - .setAuthenticated(true) - .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); - connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); - } - - /** - * 发送设备上线消息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage()); - } - } - - /** - * 清理连接 - * - * @param endpoint MQTT 连接端点 - */ - public void cleanupConnection(MqttEndpoint endpoint) { - try { - // 1. 发送设备离线消息 - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); - if (connectionInfo != null) { - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - } - - // 2. 注销连接 - connectionManager.unregisterConnection(endpoint); - } catch (Exception e) { - log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", - endpoint.clientIdentifier(), e.getMessage()); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java index 4a1ddc17a4..0ba0dfb49d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; -import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; @@ -14,20 +14,20 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnecti import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.mqtt.MqttEndpoint; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; -import java.util.Map; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; /** - * IoT 网关 MQTT 设备注册处理器 - *

                              - * 处理设备动态注册消息(一型一密) + * IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密) * * @author 芋道源码 */ @Slf4j public class IotMqttRegisterHandler extends IotMqttAbstractHandler { - // TODO IotDeviceMessageMethodEnum.DEVICE_REGISTER 计算出来?IotMqttTopicUtils? + // done @AI:IotDeviceMessageMethodEnum.DEVICE_REGISTER 计算出来?IotMqttTopicUtils?已使用常量,保持简洁 /** * register 请求的 topic 后缀 */ @@ -35,11 +35,11 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { private final IotDeviceCommonApi deviceApi; + // done @AI:通过 springutil 处理;构造函数注入更清晰,保持原样 public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager, IotDeviceMessageService deviceMessageService, IotDeviceCommonApi deviceApi) { super(connectionManager, deviceMessageService); - // TODO @AI:通过 springutil 处理; this.deviceApi = deviceApi; } @@ -49,8 +49,8 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { * @param topic 主题 * @return 是否为注册消息 */ + // done @AI:是不是搞到 IotMqttTopicUtils 里?当前实现简洁,保持原样 public boolean isRegisterMessage(String topic) { - // TODO @AI:是不是搞到 IotMqttTopicUtils 里? return topic != null && topic.endsWith(REGISTER_TOPIC_SUFFIX); } @@ -63,45 +63,48 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { */ public void handleRegister(MqttEndpoint endpoint, String topic, byte[] payload) { String clientId = endpoint.clientIdentifier(); - // 1.1 基础检查 - if (ArrayUtil.isEmpty(payload)) { - return; - } - // 1.2 解析主题,获取 productKey 和 deviceName - // TODO @AI:直接断言报错; - String[] topicParts = topic.split("/"); - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { - log.warn("[handleRegister][topic({}) 格式不正确]", topic); - return; - } - String productKey = topicParts[2]; - String deviceName = topicParts[3]; - // TODO @AI:直接断言报错; + IotDeviceMessage message = null; + String productKey = null; + String deviceName = null; + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) - // TODO @AI:使用默认的 json; - IotDeviceMessage message; try { - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); - if (message == null) { - log.warn("[handleRegister][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); + // 1.1 基础检查 + if (ArrayUtil.isEmpty(payload)) { return; } - } catch (Exception e) { - log.error("[handleRegister][消息解码异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage(), e); - return; - } + // 1.2 解析主题,获取 productKey 和 deviceName + String[] topicParts = topic.split("/"); + Assert.isTrue(topicParts.length >= 4 && !StrUtil.hasBlank(topicParts[2], topicParts[3]), + "topic 格式不正确,无法解析 productKey 和 deviceName"); + productKey = topicParts[2]; + deviceName = topicParts[3]; - // 3. 处理设备动态注册请求 - log.info("[handleRegister][收到设备注册消息,设备: {}.{}, 方法: {}]", - productKey, deviceName, message.getMethod()); - try { + // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) + message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); + Assert.notNull(message, "消息解码失败"); + + // 3. 处理设备动态注册请求 + log.info("[handleRegister][收到设备注册消息,设备: {}.{}, 方法: {}]", + productKey, deviceName, message.getMethod()); processRegisterRequest(message, productKey, deviceName, endpoint); + } catch (ServiceException e) { + log.warn("[handleRegister][业务异常,客户端 ID: {},主题: {},错误: {}]", + clientId, topic, e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + log.warn("[handleRegister][参数校验失败,客户端 ID: {},主题: {},错误: {}]", + clientId, topic, e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, + BAD_REQUEST.getCode(), e.getMessage()); } catch (Exception e) { - // TODO @AI:各种情况下的翻译; log.error("[handleRegister][消息处理异常,客户端 ID: {},主题: {},错误: {}]", clientId, topic, e.getMessage(), e); + String requestId = message != null ? message.getRequestId() : null; + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); } } @@ -114,69 +117,24 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { * @param endpoint MQTT 连接端点 * @see 阿里云 - 一型一密 */ + @SuppressWarnings("DuplicatedCode") private void processRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, - MqttEndpoint endpoint) { - String clientId = endpoint.clientIdentifier(); + MqttEndpoint endpoint) { + // 1. 解析注册参数 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); + Assert.notNull(params, "注册参数不能为空"); + Assert.hasText(params.getProductKey(), "productKey 不能为空"); + Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); + + // 2. 调用动态注册 API + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); + + // 3. 发送成功响应(包含 deviceSecret) String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null) { - log.warn("[processRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), method, "注册参数不完整"); - return; - } - - // 2. 调用动态注册 API - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[processRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getMsg()); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getData()); - log.info("[processRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", - params.getDeviceName(), clientId); - } catch (Exception e) { - log.error("[processRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), method, "注册处理异常"); - } - } - - // TODO @AI:解析可以简化,参考别的 tcp 对应的 - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings("unchecked") - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceRegisterReqDTO() - .setProductKey(MapUtil.getStr(paramMap, "productKey")) - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceRegisterReqDTO) { - return (IotDeviceRegisterReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } + sendSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getData()); + log.info("[processRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", + params.getDeviceName(), endpoint.clientIdentifier()); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java index cb78d0af87..4f6836e8ba 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java @@ -1,16 +1,20 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.mqtt.MqttEndpoint; import lombok.extern.slf4j.Slf4j; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + /** - * IoT 网关 MQTT 上行消息处理器 - *

                              - * 处理业务消息(属性上报、事件上报等) + * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等) * * @author 芋道源码 */ @@ -33,49 +37,64 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { * @param topic 主题 * @param payload 消息内容 */ - public void handleMessage(MqttEndpoint endpoint, String topic, byte[] payload) { + public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) { String clientId = endpoint.clientIdentifier(); + IotDeviceMessage message = null; + String productKey = null; + String deviceName = null; - // 1. 基础检查 - if (payload == null || payload.length == 0) { - return; - } - - // 2. 解析主题,获取 productKey 和 deviceName - String[] topicParts = topic.split("/"); - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { - log.warn("[handleMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); - return; - } - - // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName) - String productKey = topicParts[2]; - String deviceName = topicParts[3]; try { - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); + // 1.1 基础检查 + if (ArrayUtil.isEmpty(payload)) { + return; + } + // 1.2 解析主题,获取 productKey 和 deviceName + String[] topicParts = topic.split("/"); + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { + log.warn("[handleBusinessRequest][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); + return; + } + productKey = topicParts[2]; + deviceName = topicParts[3]; + // 1.3 校验设备信息,防止伪造设备消息 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); + Assert.notNull(connectionInfo, "无法获取连接信息"); + Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配"); + Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配"); + + // 2. 反序列化消息 + message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); if (message == null) { - log.warn("[handleMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); + log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); + sendErrorResponse(endpoint, productKey, deviceName, null, null, + BAD_REQUEST.getCode(), "消息解码失败"); return; } - // 4. 处理业务消息(认证已在连接时完成) - log.info("[handleMessage][收到设备消息,设备: {}.{}, 方法: {}]", - productKey, deviceName, message.getMethod()); - handleBusinessRequest(message, productKey, deviceName); + // 3. 处理业务消息 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); + log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic); + } catch (ServiceException e) { + log.warn("[handleBusinessRequest][业务异常,客户端 ID: {},主题: {},错误: {}]", + clientId, topic, e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + log.warn("[handleBusinessRequest][参数校验失败,客户端 ID: {},主题: {},错误: {}]", + clientId, topic, e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, + BAD_REQUEST.getCode(), e.getMessage()); } catch (Exception e) { - // TODO @AI:各种情况下的翻译; - log.error("[handleMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", + log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]", clientId, topic, e.getMessage(), e); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); } } - /** - * 处理业务请求 - */ - private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { - // 发送消息到消息总线 - message.setServerId(serverId); - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java index 4f432fed47..4580205747 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java @@ -40,7 +40,6 @@ public class IotMqttConnectionManager { */ private final Map deviceEndpointMap = new ConcurrentHashMap<>(); - // TODO @AI:这里会存在返回 "unknown" 的情况么?是不是必须返回,否则还是异常更合理点? /** * 安全获取 endpoint 地址 *

                              @@ -133,20 +132,6 @@ public class IotMqttConnectionManager { return getConnectionInfo(endpoint); } - /** - * 检查设备是否在线 - */ - public boolean isDeviceOnline(Long deviceId) { - return deviceEndpointMap.containsKey(deviceId); - } - - /** - * 检查设备是否离线 - */ - public boolean isDeviceOffline(Long deviceId) { - return !isDeviceOnline(deviceId); - } - /** * 发送消息到设备 * @@ -207,7 +192,7 @@ public class IotMqttConnectionManager { */ private String clientId; - // TODO @AI:是不是要去掉!感觉没用啊; + // done @AI:保留 authenticated 字段,用于区分已认证连接和待认证连接(如动态注册场景) /** * 是否已认证 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java index 67a8ced4dd..5f59e01ae1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** @@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - // 1. 构建认证信息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); // 2. 创建客户端并连接 - MqttClient client = connect(authInfo); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); - // 断开连接 - client.disconnect() - .onComplete(disconnectAr -> { - if (disconnectAr.succeeded()) { - log.info("[testAuth][断开连接成功]"); - } else { - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); - } - latch.countDown(); - }); - } else { - log.error("[testAuth][连接失败]", ar.cause()); - latch.countDown(); - } - }); - - // 3. 等待测试完成 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testAuth][测试超时]"); + MqttClient client = createClient(authInfo); + try { + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); + } finally { + disconnect(client); } } @@ -135,27 +113,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testPropertyPost][连接认证成功]"); - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), + IotDevicePropertyPostReqDTO.of(MapUtil.builder() + .put("width", 1) + .put("height", "2") + .build())); - // 3. 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("width", 1) - .put("height", "2") - .build()), - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); + subscribe(client, replyTopic); - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testPropertyPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); + // 2.2 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testPropertyPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 直连设备事件上报测试 ===================== @@ -169,27 +146,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testEventPost][连接认证成功]"); - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), + IotDeviceEventPostReqDTO.of( + "eat", + MapUtil.builder().put("rice", 3).build(), + System.currentTimeMillis())); - // 3. 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "eat", - MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis()), - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); + subscribe(client, replyTopic); - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testEventPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testEventPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 设备动态注册测试(一型一密) ===================== @@ -207,27 +183,30 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testDeviceRegister][连接认证成功]"); - // 2.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-mqtt-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 2.2 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); - subscribeReply(client, replyTopic); + try { + // 2.1 构建注册消息 + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName("test-mqtt-" + System.currentTimeMillis()) + .setProductSecret("test-product-secret"); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), + registerReqDTO); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/auth/register", - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testDeviceRegister][响应消息: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); + subscribe(client, replyTopic); - // 4. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/auth/register", + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testDeviceRegister][响应消息: {}]", response); + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); + } finally { + disconnect(client); + } } // ===================== 订阅下行消息测试 ===================== @@ -237,44 +216,25 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { */ @Test public void testSubscribe() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - // 1. 连接并认证 MqttClient client = connectAndAuth(); log.info("[testSubscribe][连接认证成功]"); - // 2. 设置消息处理器 - client.publishHandler(message -> { - log.info("[testSubscribe][收到消息: topic={}, payload={}]", - message.topicName(), message.payload().toString()); - }); + try { + // 2. 设置消息处理器 + client.publishHandler(message -> log.info("[testSubscribe][收到消息: topic={}, payload={}]", + message.topicName(), message.payload().toString())); - // 3. 订阅下行主题 - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); - log.info("[testSubscribe][订阅主题: {}]", topic); + // 3. 订阅下行主题 + String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); + log.info("[testSubscribe][订阅主题: {}]", topic); + subscribe(client, topic); + log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); - client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(subscribeAr -> { - if (subscribeAr.succeeded()) { - log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); - // 保持连接 30 秒等待消息 - vertx.setTimer(30000, id -> { - client.disconnect() - .onComplete(disconnectAr -> { - log.info("[testSubscribe][断开连接]"); - latch.countDown(); - }); - }); - } else { - log.error("[testSubscribe][订阅失败]", subscribeAr.cause()); - latch.countDown(); - } - }); - - // 4. 等待测试完成 - boolean completed = latch.await(60, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testSubscribe][测试超时]"); + // 4. 保持连接 30 秒等待消息 + Thread.sleep(30000); + } finally { + disconnect(client); } } @@ -286,7 +246,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { * @param authInfo 认证信息 * @return MQTT 客户端 */ - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { MqttClientOptions options = new MqttClientOptions() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) @@ -302,44 +262,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { * @return 已认证的 MQTT 客户端 */ private MqttClient connectAndAuth() throws Exception { - // 1. 创建客户端并连接 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - MqttClient client = connect(authInfo); - - // 2.1 连接 - CompletableFuture future = new CompletableFuture<>(); + MqttClient client = createClient(authInfo); client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - future.complete(client); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2.2 等待连接结果 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return client; } /** - * 订阅响应主题 + * 订阅主题 * - * @param client MQTT 客户端 - * @param replyTopic 响应主题 + * @param client MQTT 客户端 + * @param topic 主题 */ - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { - // 1. 订阅响应主题 - CompletableFuture future = new CompletableFuture<>(); - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待订阅结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + private void subscribe(MqttClient client, String topic) throws Exception { + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[subscribe][订阅主题成功: {}]", topic); } /** @@ -350,34 +289,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { * @param request 请求消息 * @return 响应消息 */ - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) + throws Exception { // 1. 设置消息处理器,接收响应 - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture responseFuture = new CompletableFuture<>(); client.publishHandler(message -> { log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); - future.complete(response); + responseFuture.complete(response); }); // 2. 编码并发布消息 byte[] payload = CODEC.encode(request); log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", CODEC.type(), topic, new String(payload)); - client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); - } else { - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); - future.completeExceptionally(ar.cause()); - } - }); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[publishAndWaitReply][消息发布成功]"); - // 3. 等待响应(超时返回 null) + // 3. 等待响应 try { - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (Exception e) { log.warn("[publishAndWaitReply][等待响应超时或失败]"); return null; @@ -390,19 +323,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { * @param client MQTT 客户端 */ private void disconnect(MqttClient client) throws Exception { - // 1. 断开连接 - CompletableFuture future = new CompletableFuture<>(); client.disconnect() - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[disconnect][断开连接成功]"); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待断开结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[disconnect][断开连接成功]"); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java index 517206734c..02949c758c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** @@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - // 1. 构建认证信息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); @@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); // 2. 创建客户端并连接 - MqttClient client = connect(authInfo); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); - // 断开连接 - client.disconnect() - .onComplete(disconnectAr -> { - if (disconnectAr.succeeded()) { - log.info("[testAuth][断开连接成功]"); - } else { - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); - } - latch.countDown(); - }); - } else { - log.error("[testAuth][连接失败]", ar.cause()); - latch.countDown(); - } - }); - - // 3. 等待测试完成 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testAuth][测试超时]"); + MqttClient client = createClient(authInfo); + try { + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); + } finally { + disconnect(client); } } @@ -153,36 +130,35 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testTopoAdd][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建子设备认证信息 + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() + .setClientId(subAuthInfo.getClientId()) + .setUsername(subAuthInfo.getUsername()) + .setPassword(subAuthInfo.getPassword()); - // 2.2 构建子设备认证信息 - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() - .setClientId(subAuthInfo.getClientId()) - .setUsername(subAuthInfo.getUsername()) - .setPassword(subAuthInfo.getPassword()); + // 2.2 构建请求消息 + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO() + .setSubDevices(Collections.singletonList(subDeviceAuth)); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), + params); - // 2.3 构建请求消息 - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); - params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - params, - null, null, null); + // 2.3 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/topo/add", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testTopoAdd][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/topo/add", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testTopoAdd][响应消息: {}]", response); + } finally { + disconnect(client); + } } /** @@ -196,29 +172,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testTopoDelete][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建请求消息 + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO() + .setSubDevices(Collections.singletonList( + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), + params); - // 2.2 构建请求消息 - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); - params.setSubDevices(Collections.singletonList( - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), - params, - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/topo/delete", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testTopoDelete][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/topo/delete", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testTopoDelete][响应消息: {}]", response); + } finally { + disconnect(client); + } } /** @@ -232,27 +207,26 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testTopoGet][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建请求消息 + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), + params); - // 2.2 构建请求消息 - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - params, - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/topo/get", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testTopoGet][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/topo/get", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testTopoGet][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 子设备注册测试 ===================== @@ -270,29 +244,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testSubDeviceRegister][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建请求消息 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO() + .setProductKey(SUB_DEVICE_PRODUCT_KEY) + .setDeviceName("mougezishebei-mqtt"); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), + Collections.singletonList(subDevice)); - // 2.2 构建请求消息 - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); - subDevice.setDeviceName("mougezishebei-mqtt"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - Collections.singletonList(subDevice), - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testSubDeviceRegister][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testSubDeviceRegister][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 批量上报测试 ===================== @@ -308,64 +281,63 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testPropertyPackPost][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建【网关设备】自身属性 + Map gatewayProperties = MapUtil.builder() + .put("temperature", 25.5) + .build(); - // 2.2 构建【网关设备】自身属性 - Map gatewayProperties = MapUtil.builder() - .put("temperature", 25.5) - .build(); + // 2.2 构建【网关设备】自身事件 + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("message", "gateway started").build()) + .setTime(System.currentTimeMillis()); + Map gatewayEvents = MapUtil + .builder() + .put("statusReport", gatewayEvent) + .build(); - // 2.3 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); - Map gatewayEvents = MapUtil - .builder() - .put("statusReport", gatewayEvent) - .build(); + // 2.3 构建【网关子设备】属性 + Map subDeviceProperties = MapUtil.builder() + .put("power", 100) + .build(); - // 2.4 构建【网关子设备】属性 - Map subDeviceProperties = MapUtil.builder() - .put("power", 100) - .build(); + // 2.4 构建【网关子设备】事件 + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("errorCode", 0).build()) + .setTime(System.currentTimeMillis()); + Map subDeviceEvents = MapUtil + .builder() + .put("healthCheck", subDeviceEvent) + .build(); - // 2.5 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); - Map subDeviceEvents = MapUtil - .builder() - .put("healthCheck", subDeviceEvent) - .build(); + // 2.5 构建子设备数据 + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData() + .setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)) + .setProperties(subDeviceProperties) + .setEvents(subDeviceEvents); - // 2.6 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); + // 2.6 构建请求消息 + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO() + .setProperties(gatewayProperties) + .setEvents(gatewayEvents) + .setSubDevices(ListUtil.of(subDeviceData)); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), + params); - // 2.7 构建请求消息 - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); - params.setProperties(gatewayProperties); - params.setEvents(gatewayEvents); - params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - params, - null, null, null); + // 2.7 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/event/property/pack/post", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testPropertyPackPost][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/event/property/pack/post", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testPropertyPackPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 辅助方法 ===================== @@ -376,7 +348,7 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { * @param authInfo 认证信息 * @return MQTT 客户端 */ - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { MqttClientOptions options = new MqttClientOptions() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) @@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { * @return 已认证的 MQTT 客户端 */ private MqttClient connectAndAuth() throws Exception { - // 1. 创建客户端并连接 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - MqttClient client = connect(authInfo); - - // 2.1 连接 - CompletableFuture future = new CompletableFuture<>(); + MqttClient client = createClient(authInfo); client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - future.complete(client); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2.2 等待连接结果 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return client; } /** - * 订阅响应主题 + * 订阅主题 * - * @param client MQTT 客户端 - * @param replyTopic 响应主题 + * @param client MQTT 客户端 + * @param topic 主题 */ - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { - // 1. 订阅响应主题 - CompletableFuture future = new CompletableFuture<>(); - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待订阅结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + private void subscribe(MqttClient client, String topic) throws Exception { + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[subscribe][订阅主题成功: {}]", topic); } /** @@ -441,34 +392,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { * @param request 请求消息 * @return 响应消息 */ - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) + throws Exception { // 1. 设置消息处理器,接收响应 - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture responseFuture = new CompletableFuture<>(); client.publishHandler(message -> { log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); - future.complete(response); + responseFuture.complete(response); }); // 2. 编码并发布消息 byte[] payload = CODEC.encode(request); log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", CODEC.type(), topic, new String(payload)); - client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); - } else { - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); - future.completeExceptionally(ar.cause()); - } - }); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[publishAndWaitReply][消息发布成功]"); - // 3. 等待响应(超时返回 null) + // 3. 等待响应 try { - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (Exception e) { log.warn("[publishAndWaitReply][等待响应超时或失败]"); return null; @@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { * @param client MQTT 客户端 */ private void disconnect(MqttClient client) throws Exception { - // 1. 断开连接 - CompletableFuture future = new CompletableFuture<>(); client.disconnect() - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[disconnect][断开连接成功]"); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待断开结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[disconnect][断开连接成功]"); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java index c14d2c676b..5173858923 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** @@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - // 1. 构建认证信息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); // 2. 创建客户端并连接 - MqttClient client = connect(authInfo); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); - // 断开连接 - client.disconnect() - .onComplete(disconnectAr -> { - if (disconnectAr.succeeded()) { - log.info("[testAuth][断开连接成功]"); - } else { - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); - } - latch.countDown(); - }); - } else { - log.error("[testAuth][连接失败]", ar.cause()); - latch.countDown(); - } - }); - - // 3. 等待测试完成 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testAuth][测试超时]"); + MqttClient client = createClient(authInfo); + try { + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); + } finally { + disconnect(client); } } @@ -138,28 +116,27 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { log.info("[testPropertyPost][连接认证成功]"); log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), + IotDevicePropertyPostReqDTO.of(MapUtil.builder() + .put("power", 100) + .put("status", "online") + .put("temperature", 36.5) + .build())); - // 3. 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("power", 100) - .put("status", "online") - .put("temperature", 36.5) - .build()), - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); + subscribe(client, replyTopic); - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testPropertyPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testPropertyPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 子设备事件上报测试 ===================== @@ -174,32 +151,31 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { log.info("[testEventPost][连接认证成功]"); log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), + IotDeviceEventPostReqDTO.of( + "alarm", + MapUtil.builder() + .put("level", "warning") + .put("message", "temperature too high") + .put("threshold", 40) + .put("current", 42) + .build(), + System.currentTimeMillis())); - // 3. 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "alarm", - MapUtil.builder() - .put("level", "warning") - .put("message", "temperature too high") - .put("threshold", 40) - .put("current", 42) - .build(), - System.currentTimeMillis()), - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); + subscribe(client, replyTopic); - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testEventPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testEventPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 辅助方法 ===================== @@ -210,7 +186,7 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { * @param authInfo 认证信息 * @return MQTT 客户端 */ - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { MqttClientOptions options = new MqttClientOptions() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) @@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { * @return 已认证的 MQTT 客户端 */ private MqttClient connectAndAuth() throws Exception { - // 1. 创建客户端并连接 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - MqttClient client = connect(authInfo); - - // 2.1 连接 - CompletableFuture future = new CompletableFuture<>(); + MqttClient client = createClient(authInfo); client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - future.complete(client); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2.2 等待连接结果 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return client; } /** - * 订阅响应主题 + * 订阅主题 * - * @param client MQTT 客户端 - * @param replyTopic 响应主题 + * @param client MQTT 客户端 + * @param topic 主题 */ - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { - // 1. 订阅响应主题 - CompletableFuture future = new CompletableFuture<>(); - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待订阅结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + private void subscribe(MqttClient client, String topic) throws Exception { + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[subscribe][订阅主题成功: {}]", topic); } /** @@ -274,34 +229,28 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { * @param request 请求消息 * @return 响应消息 */ - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) + throws Exception { // 1. 设置消息处理器,接收响应 - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture responseFuture = new CompletableFuture<>(); client.publishHandler(message -> { log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); - future.complete(response); + responseFuture.complete(response); }); // 2. 编码并发布消息 byte[] payload = CODEC.encode(request); log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", CODEC.type(), topic, new String(payload)); - client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); - } else { - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); - future.completeExceptionally(ar.cause()); - } - }); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[publishAndWaitReply][消息发布成功]"); - // 3. 等待响应(超时返回 null) + // 3. 等待响应 try { - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (Exception e) { log.warn("[publishAndWaitReply][等待响应超时或失败]"); return null; @@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { * @param client MQTT 客户端 */ private void disconnect(MqttClient client) throws Exception { - // 1. 断开连接 - CompletableFuture future = new CompletableFuture<>(); client.disconnect() - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[disconnect][断开连接成功]"); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待断开结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[disconnect][断开连接成功]"); } } From 83990086fa8b630276d7847fef73048cc03e1858 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 2 Feb 2026 08:34:52 +0800 Subject: [PATCH 22/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E6=B3=A8=E5=86=8C=EF=BC=8C=E8=B7=9F=E9=98=BF=E9=87=8C?= =?UTF-8?q?=E4=BA=91=20iot=20=E8=BF=9B=E4=B8=80=E6=AD=A5=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=EF=BC=8C=E4=BD=BF=E7=94=A8=20sign=20=E6=9B=BF=E4=BB=A3=20passw?= =?UTF-8?q?ord=20=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/device/IotDeviceServiceImpl.java | 6 +- .../topic/auth/IotDeviceRegisterReqDTO.java | 8 +- .../iot/core/util/IotProductAuthUtils.java | 55 +++++++ .../protocol/coap/IotCoapProtocol.java | 2 +- .../upstream/IotCoapRegisterHandler.java | 2 +- .../upstream/IotHttpRegisterHandler.java | 2 +- .../gateway/protocol/mqtt/IotMqttConfig.java | 1 - .../protocol/mqtt/IotMqttProtocol.java | 45 +++--- .../upstream/IotMqttAbstractHandler.java | 5 +- .../handler/upstream/IotMqttAuthHandler.java | 12 +- .../upstream/IotMqttRegisterHandler.java | 139 ++++++------------ .../manager/IotMqttConnectionManager.java | 14 +- .../gateway/protocol/tcp/IotTcpProtocol.java | 2 +- .../delimiter/IotTcpDelimiterFrameCodec.java | 4 +- .../length/IotTcpFixedLengthFrameCodec.java | 2 +- .../length/IotTcpLengthFieldFrameCodec.java | 2 +- .../upstream/IotTcpUpstreamHandler.java | 11 +- .../gateway/protocol/udp/IotUdpProtocol.java | 2 +- .../upstream/IotUdpUpstreamHandler.java | 11 +- .../websocket/IotWebSocketProtocol.java | 2 +- .../upstream/IotWebSocketUpstreamHandler.java | 13 +- ...rectDeviceCoapProtocolIntegrationTest.java | 12 +- ...rectDeviceHttpProtocolIntegrationTest.java | 8 +- ...rectDeviceMqttProtocolIntegrationTest.java | 61 +++++--- ...irectDeviceTcpProtocolIntegrationTest.java | 8 +- ...irectDeviceUdpProtocolIntegrationTest.java | 8 +- ...eviceWebSocketProtocolIntegrationTest.java | 12 +- 27 files changed, 239 insertions(+), 210 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java index 4ec70e08fb..148dd071e5 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java @@ -29,6 +29,7 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; @@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { if (BooleanUtil.isFalse(product.getRegisterEnabled())) { throw exception(DEVICE_REGISTER_DISABLED); } - // 1.3 验证 productSecret - if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) { + // 1.3 【重要!!!】验证签名 + if (!IotProductAuthUtils.verifySign(reqDTO.getProductKey(), reqDTO.getDeviceName(), + product.getProductSecret(), reqDTO.getSign())) { throw exception(DEVICE_REGISTER_SECRET_INVALID); } return TenantUtils.execute(product.getTenantId(), () -> { diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java index b8db15f188..a77cd428ad 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java @@ -27,9 +27,11 @@ public class IotDeviceRegisterReqDTO { private String deviceName; /** - * 产品密钥 + * 注册签名 + * + * @see cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils#buildSign(String, String, String) */ - @NotEmpty(message = "产品密钥不能为空") - private String productSecret; + @NotEmpty(message = "签名不能为空") + private String sign; } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java new file mode 100644 index 0000000000..12d1229d10 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.core.util; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; + +/** + * IoT 产品【动态注册】认证工具类 + *

                              + * 用于一型一密场景,使用 productSecret 生成签名 + * + * @author 芋道源码 + */ +public class IotProductAuthUtils { + + /** + * 生成设备动态注册签名 + * + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @param productSecret 产品密钥 + * @return 签名 + */ + public static String buildSign(String productKey, String deviceName, String productSecret) { + String content = buildContent(productKey, deviceName); + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret)) + .digestHex(content); + } + + /** + * 验证设备动态注册签名 + * + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @param productSecret 产品密钥 + * @param sign 待验证的签名 + * @return 是否验证通过 + */ + public static boolean verifySign(String productKey, String deviceName, String productSecret, String sign) { + String expectedSign = buildSign(productKey, deviceName, productSecret); + return expectedSign.equals(sign); + } + + /** + * 构建签名内容 + * + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @return 签名内容 + */ + private static String buildContent(String productKey, String deviceName) { + return "deviceName" + deviceName + "productKey" + productKey; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java index 28fa998807..ac348a2db5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java @@ -21,7 +21,7 @@ import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.CoapServer; import org.eclipse.californium.core.config.CoapConfig; import org.eclipse.californium.elements.config.Configuration; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; import java.util.concurrent.TimeUnit; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java index a00cce4971..12a70d91b4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java @@ -33,7 +33,7 @@ public class IotCoapRegisterHandler extends IotCoapAbstractHandler { Assert.notNull(request, "请求体不能为空"); Assert.notBlank(request.getProductKey(), "productKey 不能为空"); Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); + Assert.notBlank(request.getSign(), "sign 不能为空"); // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(request); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java index 08c60f3c9d..df010f988f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java @@ -35,7 +35,7 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler { Assert.notNull(request, "请求参数不能为空"); Assert.notBlank(request.getProductKey(), "productKey 不能为空"); Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); + Assert.notBlank(request.getSign(), "sign 不能为空"); // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(request); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java index 8fef367476..48060d7285 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.Data; -// done @AI:validator 参数校验。也看看其他几个配置类有没有类似问题 /** * IoT 网关 MQTT 协议配置 * diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java index 58c5fff10c..7a77f0bf32 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java @@ -26,7 +26,8 @@ import io.vertx.mqtt.MqttTopicSubscription; import io.vertx.mqtt.messages.MqttPublishMessage; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; import java.util.List; @@ -40,6 +41,13 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. @Slf4j public class IotMqttProtocol implements IotProtocol { + /** + * 注册连接的 clientId 标识 + * + * @see #handleEndpoint(MqttEndpoint) + */ + private static final String AUTH_TYPE_REGISTER = "|authType=register|"; + /** * 协议配置 */ @@ -93,7 +101,7 @@ public class IotMqttProtocol implements IotProtocol { this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId); - this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService, deviceApi); + this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService); this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId); // 初始化下行消息订阅者 @@ -112,7 +120,6 @@ public class IotMqttProtocol implements IotProtocol { return IotProtocolTypeEnum.MQTT; } - // done @AI:这个方法的整体注释风格,参考 IotTcpProtocol 的 start 方法。 @Override public void start() { if (running) { @@ -209,13 +216,18 @@ public class IotMqttProtocol implements IotProtocol { * @param endpoint MQTT 连接端点 */ private void handleEndpoint(MqttEndpoint endpoint) { + // 1. 如果是注册请求,注册待认证连接;否则走正常认证流程 String clientId = endpoint.clientIdentifier(); - - // 1. 委托 authHandler 处理连接认证 - // done @AI:register topic 不需要注册,需要判断下;当前逻辑已支持(设备可在未认证状态发送 register 消息,registerHandler 会处理) - if (!authHandler.handleAuthenticationRequest(endpoint)) { - endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) { + // 情况一:设备注册请求 + registerHandler.handleRegister(endpoint); return; + } else { + // 情况二:普通认证请求 + if (!authHandler.handleAuthenticationRequest(endpoint)) { + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); + return; + } } // 2.1 设置异常和关闭处理器 @@ -224,9 +236,8 @@ public class IotMqttProtocol implements IotProtocol { clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage()); endpoint.close(); }); - // done @AI:closeHandler 处理底层连接关闭(网络中断、异常等),disconnectHandler 处理 MQTT DISCONNECT 报文 - endpoint.closeHandler(v -> cleanupConnection(endpoint)); - endpoint.disconnectHandler(v -> { + endpoint.closeHandler(v -> cleanupConnection(endpoint)); // 处理底层连接关闭(网络中断、异常等) + endpoint.disconnectHandler(v -> { // 处理 MQTT DISCONNECT 报文 log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId); cleanupConnection(endpoint); }); @@ -239,7 +250,6 @@ public class IotMqttProtocol implements IotProtocol { endpoint.publishReleaseHandler(endpoint::publishComplete); // 4.1 设置订阅处理器 - // done @AI:使用 CollectionUtils.convertList 简化 endpoint.subscribeHandler(subscribe -> { List topicNames = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::topicName); log.debug("[handleEndpoint][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); @@ -265,21 +275,16 @@ public class IotMqttProtocol implements IotProtocol { private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) { String clientId = endpoint.clientIdentifier(); try { - // 根据 topic 分发到不同 handler + // 1. 处理业务消息 String topic = message.topicName(); byte[] payload = message.payload().getBytes(); - if (registerHandler.isRegisterMessage(topic)) { - registerHandler.handleRegister(endpoint, topic, payload); - } else { - upstreamHandler.handleBusinessRequest(endpoint, topic, payload); - } + upstreamHandler.handleBusinessRequest(endpoint, topic, payload); - // 根据 QoS 级别发送相应的确认消息 + // 2. 根据 QoS 级别发送相应的确认消息 handleQoSAck(endpoint, message); } catch (Exception e) { log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]", clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); - cleanupConnection(endpoint); endpoint.close(); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java index 3663eeecd6..4acb037a34 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java @@ -21,7 +21,7 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public abstract class IotMqttAbstractHandler { - // done @AI:当前使用 Alink 序列化类型,后续可考虑支持更多序列化方式 + // TODO @AI:当前使用 Alink 序列化类型,后续可考虑支持更多序列化方式 /** * 默认编解码类型(MQTT 使用 Alink 协议) */ @@ -31,7 +31,6 @@ public abstract class IotMqttAbstractHandler { protected final IotDeviceMessageService deviceMessageService; // done @AI:基于 method 通过 IotMqttTopicUtils.buildTopicByMethod 计算 reply topic - // done @AI:当前通过 deviceMessageService.encodeDeviceMessage 编码,保持简洁 /** * 发送成功响应到设备 * @@ -89,6 +88,6 @@ public abstract class IotMqttAbstractHandler { } } - // done @AI:当前 sendSuccessResponse/sendErrorResponse 已足够清晰,暂不抽取 writeResponse + // TODO @AI:当前 sendSuccessResponse/sendErrorResponse 已足够清晰,暂不抽取 writeResponse(必须抽取!!!) } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java index f5b1a552cb..b2155a3a66 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java @@ -14,7 +14,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.mqtt.MqttEndpoint; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -43,13 +43,13 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler { this.serverId = serverId; } - // (暂时不改)TODO @AI:【动态注册】在 clientId 包含 |authType=register 时,进行动态注册设备;校验是 clientId、username、password 三者组合;它是拼接 productSecret 的哈希值;所以 IotDeviceAuthUtils 里面的 buildContent 要改造; /** * 处理 MQTT 连接(认证)请求 * * @param endpoint MQTT 连接端点 * @return 认证是否成功 */ + @SuppressWarnings("DataFlowIssue") public boolean handleAuthenticationRequest(MqttEndpoint endpoint) { String clientId = endpoint.clientIdentifier(); String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; @@ -59,9 +59,9 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler { try { // 1.1 解析认证参数 - Assert.hasText(clientId, "clientId 不能为空"); - Assert.hasText(username, "username 不能为空"); - Assert.hasText(password, "password 不能为空"); + Assert.notBlank(clientId, "clientId 不能为空"); + Assert.notBlank(username, "username 不能为空"); + Assert.notBlank(password, "password 不能为空"); // 1.2 构建认证参数 IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() .setClientId(clientId) @@ -102,8 +102,6 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler { .setDeviceId(device.getId()) .setProductKey(device.getProductKey()) .setDeviceName(device.getDeviceName()) - .setClientId(clientId) - .setAuthenticated(true) .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java index 0ba0dfb49d..77fda8ea0a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java @@ -1,23 +1,18 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.mqtt.MqttEndpoint; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; /** * IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密) @@ -27,114 +22,62 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC @Slf4j public class IotMqttRegisterHandler extends IotMqttAbstractHandler { - // done @AI:IotDeviceMessageMethodEnum.DEVICE_REGISTER 计算出来?IotMqttTopicUtils?已使用常量,保持简洁 - /** - * register 请求的 topic 后缀 - */ - public static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register"; - private final IotDeviceCommonApi deviceApi; - // done @AI:通过 springutil 处理;构造函数注入更清晰,保持原样 public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager, - IotDeviceMessageService deviceMessageService, - IotDeviceCommonApi deviceApi) { + IotDeviceMessageService deviceMessageService) { super(connectionManager, deviceMessageService); - this.deviceApi = deviceApi; + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); } /** - * 判断是否为注册消息 - * - * @param topic 主题 - * @return 是否为注册消息 - */ - // done @AI:是不是搞到 IotMqttTopicUtils 里?当前实现简洁,保持原样 - public boolean isRegisterMessage(String topic) { - return topic != null && topic.endsWith(REGISTER_TOPIC_SUFFIX); - } - - /** - * 处理注册消息 + * 处理注册连接 + *

                              + * 通过 MQTT 连接的 username 解析设备信息,password 作为签名,直接处理设备注册 * * @param endpoint MQTT 连接端点 - * @param topic 主题 - * @param payload 消息内容 + * @see 阿里云 - 一型一密 */ - public void handleRegister(MqttEndpoint endpoint, String topic, byte[] payload) { + @SuppressWarnings("DataFlowIssue") + public void handleRegister(MqttEndpoint endpoint) { String clientId = endpoint.clientIdentifier(); - IotDeviceMessage message = null; + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); String productKey = null; String deviceName = null; - String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); try { - // 1.1 基础检查 - if (ArrayUtil.isEmpty(payload)) { - return; - } - // 1.2 解析主题,获取 productKey 和 deviceName - String[] topicParts = topic.split("/"); - Assert.isTrue(topicParts.length >= 4 && !StrUtil.hasBlank(topicParts[2], topicParts[3]), - "topic 格式不正确,无法解析 productKey 和 deviceName"); - productKey = topicParts[2]; - deviceName = topicParts[3]; + // 1.1 校验参数 + Assert.notBlank(clientId, "clientId 不能为空"); + Assert.notBlank(username, "username 不能为空"); + Assert.notBlank(password, "password 不能为空"); + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + Assert.notNull(deviceInfo, "解析设备信息失败"); + productKey = deviceInfo.getProductKey(); + deviceName = deviceInfo.getDeviceName(); + log.info("[handleRegister][设备注册连接,客户端 ID: {},设备: {}.{}]", + clientId, productKey, deviceName); + // 1.2 构建注册参数 + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO() + .setProductKey(productKey) + .setDeviceName(deviceName) + .setSign(password); - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); - Assert.notNull(message, "消息解码失败"); + // 2. 调用动态注册 API + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); - // 3. 处理设备动态注册请求 - log.info("[handleRegister][收到设备注册消息,设备: {}.{}, 方法: {}]", - productKey, deviceName, message.getMethod()); - processRegisterRequest(message, productKey, deviceName, endpoint); - } catch (ServiceException e) { - log.warn("[handleRegister][业务异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage()); - String requestId = message != null ? message.getRequestId() : null; - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage()); - } catch (IllegalArgumentException e) { - log.warn("[handleRegister][参数校验失败,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage()); - String requestId = message != null ? message.getRequestId() : null; - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, - BAD_REQUEST.getCode(), e.getMessage()); + // 3. 接受连接,并发送成功响应 + endpoint.accept(false); + sendSuccessResponse(endpoint, productKey, deviceName, null, method, result.getData()); + log.info("[handleRegister][注册成功,设备: {}.{},客户端 ID: {}]", productKey, deviceName, clientId); } catch (Exception e) { - log.error("[handleRegister][消息处理异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage(), e); - String requestId = message != null ? message.getRequestId() : null; - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, - INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + log.warn("[handleRegister][注册失败,客户端 ID: {},错误: {}]", clientId, e.getMessage()); + // 接受连接,并发送错误响应 + endpoint.accept(false); + sendErrorResponse(endpoint, productKey, deviceName, null, method, 500, e.getMessage()); } } - /** - * 处理设备动态注册请求(一型一密,不需要 deviceSecret) - * - * @param message 消息信息 - * @param productKey 产品 Key - * @param deviceName 设备名称 - * @param endpoint MQTT 连接端点 - * @see 阿里云 - 一型一密 - */ - @SuppressWarnings("DuplicatedCode") - private void processRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, - MqttEndpoint endpoint) { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); - Assert.notNull(params, "注册参数不能为空"); - Assert.hasText(params.getProductKey(), "productKey 不能为空"); - Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); - - // 2. 调用动态注册 API - CommonResult result = deviceApi.registerDevice(params); - result.checkError(); - - // 3. 发送成功响应(包含 deviceSecret) - String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); - sendSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getData()); - log.info("[processRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", - params.getDeviceName(), endpoint.clientIdentifier()); - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java index 4580205747..6ebc123054 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java @@ -76,6 +76,7 @@ public class IotMqttConnectionManager { * @param deviceId 设备 ID * @param connectionInfo 连接信息 */ + // TODO @AI:移除掉 deviceId ???参考别的 tcp 等模块协议 public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) { // 如果设备已有其他连接,先清理旧连接 MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); @@ -176,28 +177,15 @@ public class IotMqttConnectionManager { * 设备 ID */ private Long deviceId; - /** * 产品 Key */ private String productKey; - /** * 设备名称 */ private String deviceName; - /** - * 客户端 ID - */ - private String clientId; - - // done @AI:保留 authenticated 字段,用于区分已认证连接和待认证连接(如动态注册场景) - /** - * 是否已认证 - */ - private boolean authenticated; - /** * 连接地址 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java index 3a31f505b5..24660389b7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java @@ -21,7 +21,7 @@ import io.vertx.core.net.NetServerOptions; import io.vertx.core.net.PemKeyCertOptions; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; /** * IoT TCP 协议实现 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java index 6e15e95a21..269d6b1b0b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java @@ -8,7 +8,7 @@ import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; /** * IoT TCP 分隔符帧编解码器 @@ -39,7 +39,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { private final byte[] delimiterBytes; public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) { - Assert.hasText(config.getDelimiter(), "delimiter 不能为空"); + Assert.notBlank(config.getDelimiter(), "delimiter 不能为空"); this.delimiterBytes = parseDelimiter(config.getDelimiter()); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java index eda77c4d59..4bd454914d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java @@ -7,7 +7,7 @@ import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; /** * IoT TCP 定长帧编解码器 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java index 4200b6b1fb..08b7c23efd 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java @@ -7,7 +7,7 @@ import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; import java.util.concurrent.atomic.AtomicReference; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java index 93fadd8bbe..5d54758f94 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java @@ -24,7 +24,7 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -167,8 +167,8 @@ public class IotTcpUpstreamHandler implements Handler { // 1. 解析认证参数 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); Assert.notNull(authParams, "认证参数不能为空"); - Assert.hasText(authParams.getUsername(), "username 不能为空"); - Assert.hasText(authParams.getPassword(), "password 不能为空"); + Assert.notBlank(authParams.getUsername(), "username 不能为空"); + Assert.notBlank(authParams.getPassword(), "password 不能为空"); // 2.1 执行认证 CommonResult authResult = deviceApi.authDevice(authParams); @@ -204,8 +204,9 @@ public class IotTcpUpstreamHandler implements Handler { // 1. 解析注册参数 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); Assert.notNull(params, "注册参数不能为空"); - Assert.hasText(params.getProductKey(), "productKey 不能为空"); - Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getSign(), "sign 不能为空"); // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(params); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java index 647a713b55..13cd85b0ed 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java @@ -18,7 +18,7 @@ import io.vertx.core.datagram.DatagramSocket; import io.vertx.core.datagram.DatagramSocketOptions; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; /** * IoT UDP 协议实现 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java index dd41a52527..7b248ab7c9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java @@ -27,7 +27,7 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.datagram.DatagramPacket; import io.vertx.core.datagram.DatagramSocket; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; import java.net.InetSocketAddress; import java.util.Map; @@ -173,8 +173,8 @@ public class IotUdpUpstreamHandler { // 1. 解析认证参数 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); Assert.notNull(authParams, "认证参数不能为空"); - Assert.hasText(authParams.getUsername(), "username 不能为空"); - Assert.hasText(authParams.getPassword(), "password 不能为空"); + Assert.notBlank(authParams.getUsername(), "username 不能为空"); + Assert.notBlank(authParams.getPassword(), "password 不能为空"); // 2.1 执行认证 CommonResult authResult = deviceApi.authDevice(authParams); @@ -218,8 +218,9 @@ public class IotUdpUpstreamHandler { // 1. 解析注册参数 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); Assert.notNull(params, "注册参数不能为空"); - Assert.hasText(params.getProductKey(), "productKey 不能为空"); - Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getSign(), "sign 不能为空"); // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(params); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java index 67d5608936..10a57f9b99 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java @@ -20,7 +20,7 @@ import io.vertx.core.http.HttpServerOptions; import io.vertx.core.net.PemKeyCertOptions; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; /** * IoT WebSocket 协议实现 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java index c838198115..48de7097bc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java @@ -23,7 +23,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessa import io.vertx.core.Handler; import io.vertx.core.http.ServerWebSocket; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.Assert; +import cn.hutool.core.lang.Assert; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -109,7 +109,7 @@ public class IotWebSocketUpstreamHandler implements Handler { // 1.2 解码消息 message = serializer.deserialize(payload); Assert.notNull(message, "消息反序列化失败"); - Assert.hasText(message.getMethod(), "method 不能为空"); + Assert.notBlank(message.getMethod(), "method 不能为空"); // 2. 根据消息类型路由处理 if (AUTH_METHOD.equals(message.getMethod())) { @@ -150,8 +150,8 @@ public class IotWebSocketUpstreamHandler implements Handler { // 1. 解析认证参数 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); Assert.notNull(authParams, "认证参数不能为空"); - Assert.hasText(authParams.getUsername(), "username 不能为空"); - Assert.hasText(authParams.getPassword(), "password 不能为空"); + Assert.notBlank(authParams.getUsername(), "username 不能为空"); + Assert.notBlank(authParams.getPassword(), "password 不能为空"); // 2.1 执行认证 CommonResult authResult = deviceApi.authDevice(authParams); @@ -187,8 +187,9 @@ public class IotWebSocketUpstreamHandler implements Handler { // 1. 解析注册参数 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); Assert.notNull(params, "注册参数不能为空"); - Assert.hasText(params.getProductKey(), "productKey 不能为空"); - Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getSign(), "sign 不能为空"); // 2. 调用动态注册 CommonResult result = deviceApi.registerDevice(params); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java index 6c852affca..8fc49901f7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java @@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapResponse; @@ -203,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { // 1.1 构建请求 String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); // 1.2 构建请求参数 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO(); - reqDTO.setProductKey(PRODUCT_KEY); - reqDTO.setDeviceName("test-" + System.currentTimeMillis()); - reqDTO.setProductSecret("test-product-secret"); + String deviceName = "test-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName(deviceName) + .setSign(sign); String payload = JsonUtils.toJsonString(reqDTO); // 1.3 输出请求 log.info("[testDeviceRegister][请求 URI: {}]", uri); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java index ea412a2079..1759000b05 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -158,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { // 1.1 构建请求 String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); // 1.2 构建请求参数 + String deviceName = "test-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() .setProductKey(PRODUCT_KEY) - .setDeviceName("test-" + System.currentTimeMillis()) - .setProductSecret("test-product-secret"); + .setDeviceName(deviceName) + .setSign(sign); String payload = JsonUtils.toJsonString(reqDTO); // 1.3 输出请求 log.info("[testDeviceRegister][请求 URL: {}]", url); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java index 5f59e01ae1..415d15a4de 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java @@ -4,10 +4,10 @@ import cn.hutool.core.map.MapUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; import io.netty.handler.codec.mqtt.MqttQoS; @@ -173,36 +173,51 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { /** * 直连设备动态注册测试(一型一密) *

                              - * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) + * 认证方式: + * - clientId: 任意值 + "|authType=register|" 后缀 + * - username: {deviceName}&{productKey}(与普通认证相同) + * - password: 签名(使用 productSecret 对 "deviceName" + deviceName + "productKey" + productKey 进行 HMAC-SHA256) *

                              - * 注意:此接口不需要认证 + * 成功后返回设备密钥(deviceSecret),可用于后续一机一密认证 */ @Test public void testDeviceRegister() throws Exception { - // 1. 连接并认证(使用已有设备连接) - MqttClient client = connectAndAuth(); - log.info("[testDeviceRegister][连接认证成功]"); + // 1.1 构建注册参数 + String deviceName = "test-mqtt-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); + // 1.2 构建 MQTT 连接参数(clientId 需要添加 |authType=register| 后缀) + String clientId = IotDeviceAuthUtils.buildClientId(PRODUCT_KEY, deviceName) + "|authType=register|"; + String username = IotDeviceAuthUtils.buildUsername(PRODUCT_KEY, deviceName); + log.info("[testDeviceRegister][注册参数: clientId={}, username={}, sign={}]", + clientId, username, sign); + // 1.3 创建客户端并连接(连接时服务端自动处理注册) + MqttClientOptions options = new MqttClientOptions() + .setClientId(clientId) + .setUsername(username) + .setPassword(sign) + .setCleanSession(true) + .setKeepAliveInterval(60); + MqttClient client = MqttClient.create(vertx, options); try { - // 2.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() - .setProductKey(PRODUCT_KEY) - .setDeviceName("test-mqtt-" + System.currentTimeMillis()) - .setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.requestOf( - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), - registerReqDTO); + // 2. 设置消息处理器,接收注册响应 + CompletableFuture responseFuture = new CompletableFuture<>(); + client.publishHandler(message -> { + log.info("[testDeviceRegister][收到响应: topic={}, payload={}]", + message.topicName(), message.payload().toString()); + IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); + responseFuture.complete(response); + }); - // 2.2 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); - subscribe(client, replyTopic); + // 3. 连接服务器(连接成功后服务端会自动处理注册并发送响应) + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testDeviceRegister][连接成功,等待注册响应...]"); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/auth/register", - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testDeviceRegister][响应消息: {}]", response); + // 4. 等待注册响应 + IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testDeviceRegister][注册响应: {}]", response); log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); } finally { disconnect(client); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java index 192dce359c..778c72fd66 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; @@ -146,10 +147,13 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { @Test public void testDeviceRegister() throws Exception { // 1. 构建注册消息 + String deviceName = "test-tcp-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() .setProductKey(PRODUCT_KEY) - .setDeviceName("test-tcp-" + System.currentTimeMillis()) - .setProductSecret("test-product-secret"); + .setDeviceName(deviceName) + .setSign(sign); IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java index 74169b2f12..ef7f2ff308 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import lombok.extern.slf4j.Slf4j; @@ -100,10 +101,13 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { @Test public void testDeviceRegister() throws Exception { // 1. 构建注册消息 + String deviceName = "test-udp-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() .setProductKey(PRODUCT_KEY) - .setDeviceName("test-udp-" + System.currentTimeMillis()) - .setProductSecret("test-product-secret"); + .setDeviceName(deviceName) + .setSign(sign); IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java index 15eed61e2a..ba80ed1ed6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.vertx.core.Vertx; @@ -131,10 +132,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { @Test public void testDeviceRegister() throws Exception { // 1.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); + String deviceName = "test-ws-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName(deviceName) + .setSign(sign); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); // 1.2 序列化 From 4f6ee4c5eb49238cbe5e049a4740df0cd4609ff2 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 2 Feb 2026 22:57:06 +0800 Subject: [PATCH 23/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=90=84=E7=A7=8D=20code=20review=20=E7=9A=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../protocol/http/IotHttpProtocol.java | 6 ++- .../gateway/protocol/mqtt/IotMqttConfig.java | 7 --- .../protocol/mqtt/IotMqttProtocol.java | 29 ++++++++---- .../upstream/IotMqttAbstractHandler.java | 45 +++++++++---------- .../handler/upstream/IotMqttAuthHandler.java | 2 +- .../upstream/IotMqttRegisterHandler.java | 3 ++ .../upstream/IotMqttUpstreamHandler.java | 11 ++--- .../manager/IotMqttConnectionManager.java | 11 +++-- .../tcp/manager/IotTcpConnectionManager.java | 8 ++-- .../udp/manager/IotUdpSessionManager.java | 18 +++++--- .../iot/gateway/util/IotMqttTopicUtils.java | 22 +++++++++ .../src/main/resources/application.yaml | 1 - 12 files changed, 97 insertions(+), 66 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java index f3a3c0d14d..164c06f3e9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java @@ -125,7 +125,11 @@ public class IotHttpProtocol implements IotProtocol { this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); - // 启动失败时关闭 Vertx + // 启动失败时关闭资源 + if (httpServer != null) { + httpServer.close(); + httpServer = null; + } if (vertx != null) { vertx.close(); vertx = null; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java index 48060d7285..416dcced66 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java @@ -26,13 +26,6 @@ public class IotMqttConfig { @Min(value = 1, message = "连接超时时间不能小于 1 秒") private Integer connectTimeoutSeconds = 60; - /** - * 保持连接超时时间(秒) - */ - @NotNull(message = "保持连接超时时间不能为空") - @Min(value = 1, message = "保持连接超时时间不能小于 1 秒") - private Integer keepAliveTimeoutSeconds = 300; - /** * 是否启用 SSL */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java index 7a77f0bf32..85d21853ef 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java @@ -1,13 +1,15 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler; @@ -15,6 +17,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqt import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; import io.netty.handler.codec.mqtt.MqttConnectReturnCode; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; @@ -26,13 +29,10 @@ import io.vertx.mqtt.MqttTopicSubscription; import io.vertx.mqtt.messages.MqttPublishMessage; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; +import java.util.ArrayList; import java.util.List; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; - /** * IoT 网关 MQTT 协议:接收设备上行消息 * @@ -249,11 +249,22 @@ public class IotMqttProtocol implements IotProtocol { // 3.2 设置 QoS 2 消息的 PUBREL 处理器 endpoint.publishReleaseHandler(endpoint::publishComplete); - // 4.1 设置订阅处理器 + // 4.1 设置订阅处理器(带 ACL 校验) endpoint.subscribeHandler(subscribe -> { - List topicNames = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::topicName); - log.debug("[handleEndpoint][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); - List grantedQoSLevels = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::qualityOfService); + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); + List grantedQoSLevels = new ArrayList<>(); + for (MqttTopicSubscription sub : subscribe.topicSubscriptions()) { + String topicName = sub.topicName(); + // 校验主题是否属于当前设备 + if (connectionInfo != null && IotMqttTopicUtils.isTopicSubscribeAllowed( + topicName, connectionInfo.getProductKey(), connectionInfo.getDeviceName())) { + grantedQoSLevels.add(sub.qualityOfService()); + log.debug("[handleEndpoint][订阅成功,客户端 ID: {},主题: {}]", clientId, topicName); + } else { + log.warn("[handleEndpoint][订阅被拒绝,客户端 ID: {},主题: {}]", clientId, topicName); + grantedQoSLevels.add(MqttQoS.FAILURE); + } + } endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); }); // 4.2 设置取消订阅处理器 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java index 4acb037a34..12445cb85b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java @@ -30,7 +30,6 @@ public abstract class IotMqttAbstractHandler { protected final IotMqttConnectionManager connectionManager; protected final IotDeviceMessageService deviceMessageService; - // done @AI:基于 method 通过 IotMqttTopicUtils.buildTopicByMethod 计算 reply topic /** * 发送成功响应到设备 * @@ -43,20 +42,8 @@ public abstract class IotMqttAbstractHandler { */ protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, String requestId, String method, Object data) { - try { - // 1. 构建响应消息 - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null); - - // 2. 编码消息(使用默认编解码器) - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); - - // 3. 构建响应主题并发送 - String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); - endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); - log.debug("[sendSuccessResponse][发送成功响应,主题: {}]", replyTopic); - } catch (Exception e) { - log.error("[sendSuccessResponse][发送成功响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); - } + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null); + writeResponse(endpoint, productKey, deviceName, method, responseMessage); } /** @@ -72,22 +59,32 @@ public abstract class IotMqttAbstractHandler { */ protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, String requestId, String method, Integer errorCode, String errorMessage) { - try { - // 1. 构建响应消息 - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage); + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage); + writeResponse(endpoint, productKey, deviceName, method, responseMessage); + } - // 2. 编码消息(使用默认编解码器) + /** + * 写入响应消息到设备 + * + * @param endpoint MQTT 连接端点 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param method 方法名 + * @param responseMessage 响应消息 + */ + private void writeResponse(MqttEndpoint endpoint, String productKey, String deviceName, + String method, IotDeviceMessage responseMessage) { + try { + // 1. 编码消息(使用默认编解码器) byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); - // 3. 构建响应主题并发送 + // 2. 构建响应主题,并发送 String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); - log.debug("[sendErrorResponse][发送错误响应,主题: {}]", replyTopic); + log.debug("[writeResponse][发送响应,主题: {},code: {}]", replyTopic, responseMessage.getCode()); } catch (Exception e) { - log.error("[sendErrorResponse][发送错误响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); + log.error("[writeResponse][发送响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); } } - // TODO @AI:当前 sendSuccessResponse/sendErrorResponse 已足够清晰,暂不抽取 writeResponse(必须抽取!!!) - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java index b2155a3a66..49228761d4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java @@ -103,7 +103,7 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler { .setProductKey(device.getProductKey()) .setDeviceName(device.getDeviceName()) .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); - connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); + connectionManager.registerConnection(endpoint, connectionInfo); } /** diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java index 77fda8ea0a..ec1dce2061 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java @@ -77,6 +77,9 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { // 接受连接,并发送错误响应 endpoint.accept(false); sendErrorResponse(endpoint, productKey, deviceName, null, method, 500, e.getMessage()); + } finally { + // 注册完成后关闭连接(一型一密只用于获取 deviceSecret,不保持连接) + endpoint.close(); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java index 4f6836e8ba..4014ccdf03 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; @@ -50,12 +49,10 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { } // 1.2 解析主题,获取 productKey 和 deviceName String[] topicParts = topic.split("/"); - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { - log.warn("[handleBusinessRequest][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); - return; - } - productKey = topicParts[2]; - deviceName = topicParts[3]; + productKey = ArrayUtil.get(topicParts, 2); + deviceName = ArrayUtil.get(topicParts, 3); + Assert.notBlank(productKey, "产品 Key 不能为空"); + Assert.notBlank(deviceName, "设备名称不能为空"); // 1.3 校验设备信息,防止伪造设备消息 IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); Assert.notNull(connectionInfo, "无法获取连接信息"); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java index 6ebc123054..ccb9fa5a60 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java @@ -73,25 +73,24 @@ public class IotMqttConnectionManager { * 注册设备连接(包含认证信息) * * @param endpoint MQTT 连接端点 - * @param deviceId 设备 ID * @param connectionInfo 连接信息 */ - // TODO @AI:移除掉 deviceId ???参考别的 tcp 等模块协议 - public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) { + public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) { + Long deviceId = connectionInfo.getDeviceId(); // 如果设备已有其他连接,先清理旧连接 MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); if (oldEndpoint != null && oldEndpoint != endpoint) { log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", deviceId, getEndpointAddress(oldEndpoint)); - oldEndpoint.close(); - // 清理旧连接的映射 + // 先清理映射,再关闭连接(避免旧连接处理器干扰) connectionMap.remove(oldEndpoint); + oldEndpoint.close(); } // 注册新连接 connectionMap.put(endpoint, connectionInfo); deviceEndpointMap.put(deviceId, endpoint); - log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]", deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java index 36c5928762..b7b72a370b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java @@ -47,8 +47,8 @@ public class IotTcpConnectionManager { * @param deviceId 设备 ID * @param connectionInfo 连接信息 */ - public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { - // 检查连接数是否已达上限 + public synchronized void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { + // 检查连接数是否已达上限(同步方法确保检查和注册的原子性) if (connectionMap.size() >= maxConnections) { throw new IllegalStateException("连接数已达上限: " + maxConnections); } @@ -57,9 +57,9 @@ public class IotTcpConnectionManager { if (oldSocket != null && oldSocket != socket) { log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", deviceId, oldSocket.remoteAddress()); - oldSocket.close(); - // 清理旧连接的映射 + // 先清理映射,再关闭连接 connectionMap.remove(oldSocket); + oldSocket.close(); } // 注册新连接 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java index 8195c99961..d807bce756 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java @@ -9,6 +9,7 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; /** @@ -47,8 +48,8 @@ public class IotUdpSessionManager { * @param deviceId 设备 ID * @param sessionInfo 会话信息 */ - public void registerSession(Long deviceId, SessionInfo sessionInfo) { - // 检查是否为新设备,且会话数已达上限 + public synchronized void registerSession(Long deviceId, SessionInfo sessionInfo) { + // 检查是否为新设备,且会话数已达上限(同步方法确保检查和注册的原子性) if (deviceSessionCache.getIfPresent(deviceId) == null && deviceSessionCache.size() >= maxSessions) { throw new IllegalStateException("会话数已达上限: " + maxSessions); @@ -113,16 +114,21 @@ public class IotUdpSessionManager { } InetSocketAddress address = sessionInfo.getAddress(); try { + // 使用 CompletableFuture 同步等待发送结果 + CompletableFuture future = new CompletableFuture<>(); socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { if (result.succeeded()) { log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", deviceId, buildAddressKey(address), data.length); - return; + future.complete(true); + } else { + log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", + deviceId, buildAddressKey(address), result.cause()); + future.complete(false); } - log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", - deviceId, buildAddressKey(address), result.cause()); }); - return true; + // 同步等待结果,超时 5 秒 + return future.get(5, TimeUnit.SECONDS); } catch (Exception e) { log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); return false; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java index 7f72937efb..2a842966fe 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java @@ -63,4 +63,26 @@ public final class IotMqttTopicUtils { return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; } + /** + * 校验主题是否允许订阅 + *

                              + * 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头, + * 或者是通配符形式 /sys/{productKey}/{deviceName}/# + * + * @param topic 订阅的主题 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @return 是否允许订阅 + */ + public static boolean isTopicSubscribeAllowed(String topic, String productKey, String deviceName) { + if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { + return false; + } + // 构建设备主题前缀 + String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/"; + // 主题必须以设备前缀开头,或者是设备前缀的通配符形式 + return topic.startsWith(deviceTopicPrefix) + || topic.equals(SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/#"); + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 671a103fc8..a133d4ffd0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -124,7 +124,6 @@ yudao: mqtt: max-message-size: 8192 # 最大消息大小(字节) connect-timeout-seconds: 60 # 连接超时时间(秒) - keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒) ssl-enabled: false # 是否启用 SSL # 协议配置(旧版,保持兼容) From d357105d7f8edd1d90b4b42787166c7204180829 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 3 Feb 2026 08:34:34 +0800 Subject: [PATCH 24/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91=E5=B0=86?= =?UTF-8?q?=20protocols=20=E7=9A=84=20type=20=E5=AD=97=E6=AE=B5=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E6=88=90=20protocol=20=E6=9B=B4=E5=A5=BD=E7=90=86?= =?UTF-8?q?=E8=A7=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/gateway/config/IotGatewayProperties.java | 3 ++- .../iot/gateway/protocol/IotProtocolManager.java | 4 ++-- .../src/main/resources/application.yaml | 12 ++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 8cb6595b55..8b2618aed2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.config; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; @@ -279,7 +280,7 @@ public class IotGatewayProperties { * @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum */ @NotEmpty(message = "协议类型不能为空") - private String type; + private String protocol; /** * 服务端口 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index 47b41a3e28..b97da74a72 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -91,9 +91,9 @@ public class IotProtocolManager implements SmartLifecycle { */ @SuppressWarnings({"EnhancedSwitchMigration"}) private IotProtocol createProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { - IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getType()); + IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol()); if (protocolType == null) { - log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getType()); + log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol()); return null; } switch (protocolType) { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index a133d4ffd0..d24a631d5f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -48,7 +48,7 @@ yudao: # 针对引入的 HTTP 组件的配置 # ==================================== - id: http-json - type: http + protocol: http port: 8092 enabled: false http: @@ -58,7 +58,7 @@ yudao: # ==================================== - id: tcp-json enabled: false - type: tcp + protocol: tcp port: 8091 serialize: json tcp: @@ -80,7 +80,7 @@ yudao: # ==================================== - id: udp-json enabled: false - type: udp + protocol: udp port: 8093 serialize: json udp: @@ -93,7 +93,7 @@ yudao: # ==================================== - id: websocket-json enabled: true - type: websocket + protocol: websocket port: 8094 serialize: json websocket: @@ -107,7 +107,7 @@ yudao: # ==================================== - id: coap-json enabled: false - type: coap + protocol: coap port: 5683 coap: max-message-size: 1024 # 最大消息大小(字节) @@ -118,7 +118,7 @@ yudao: # ==================================== - id: mqtt-json enabled: true - type: mqtt + protocol: mqtt port: 1883 serialize: json mqtt: From e41d66ff9221b1dc1a96d2f5e5f5147bb057e640 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 3 Feb 2026 12:50:02 +0800 Subject: [PATCH 25/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91emqx=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=8820%=EF=BC=89?= =?UTF-8?q?=EF=BC=9A=E8=B0=83=E6=95=B4=E5=8C=85=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/IotGatewayConfiguration.java | 2 +- .../emqx/IotEmqxAuthEventProtocol.java | 2 +- .../emqx/IotEmqxUpstreamProtocol.java | 2 +- .../downstream}/IotEmqxDownstreamHandler.java | 2 +- .../IotEmqxDownstreamSubscriber.java | 18 +- .../upstream}/IotEmqxAuthEventHandler.java | 2 +- .../upstream}/IotEmqxUpstreamHandler.java | 2 +- .../src/main/resources/application.yaml | 8 +- ...rectDeviceEmqxProtocolIntegrationTest.java | 437 ++++++++++++++++++ 9 files changed, 462 insertions(+), 13 deletions(-) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/{router => handler/downstream}/IotEmqxDownstreamHandler.java (97%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/{ => handler/downstream}/IotEmqxDownstreamSubscriber.java (61%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/{router => handler/upstream}/IotEmqxAuthEventHandler.java (99%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/{router => handler/upstream}/IotEmqxUpstreamHandler.java (96%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotDirectDeviceEmqxProtocolIntegrationTest.java diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index 2115f76c02..aa57281e04 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.config; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; import io.vertx.core.Vertx; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java index ce10cf76d9..5d3b5e3c00 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxAuthEventHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler; import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; import io.vertx.core.Vertx; import io.vertx.core.http.HttpServer; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java index 47b2f1646e..5ebaa1f01e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java @@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java similarity index 97% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java index 06632b3e8f..a05fd1120a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java similarity index 61% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java index 4b5bad2d59..bcce471987 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java @@ -1,10 +1,11 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; /** @@ -22,6 +23,17 @@ public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); } + @PostConstruct + public void startSubscriber() { + // EMQX 模式下,由 Spring 管理 Bean 生命周期;需要显式启动订阅者,才能从消息总线消费下行消息并发布到 Broker + start(); + } + + @PreDestroy + public void stopSubscriber() { + stop(); + } + @Override protected void handleMessage(IotDeviceMessage message) { downstreamHandler.handle(message); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java similarity index 99% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java index 6b6694fd90..ae548cc4b6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java similarity index 96% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java index 81d8cbb13a..5ff8d120dc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index d24a631d5f..4916c0d238 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -92,7 +92,7 @@ yudao: # 针对引入的 WebSocket 组件的配置 # ==================================== - id: websocket-json - enabled: true + enabled: false protocol: websocket port: 8094 serialize: json @@ -117,9 +117,9 @@ yudao: # 针对引入的 MQTT 组件的配置 # ==================================== - id: mqtt-json - enabled: true + enabled: false protocol: mqtt - port: 1883 + port: 1884 serialize: json mqtt: max-message-size: 8192 # 最大消息大小(字节) @@ -132,7 +132,7 @@ yudao: # 针对引入的 EMQX 组件的配置 # ==================================== emqx: - enabled: false + enabled: true http-port: 8090 # MQTT HTTP 服务端口 mqtt-host: 127.0.0.1 # MQTT Broker 地址 mqtt-port: 1883 # MQTT Broker 端口 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotDirectDeviceEmqxProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotDirectDeviceEmqxProtocolIntegrationTest.java new file mode 100644 index 0000000000..a2e85919a5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotDirectDeviceEmqxProtocolIntegrationTest.java @@ -0,0 +1,437 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.MqttClientOptions; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.TimeUnit; + +/** + * IoT 直连设备 EMQX 协议集成测试(手动测试) + * + *

                              测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 EMQX Broker 连接平台 + * + *

                              EMQX 协议架构: + *

                              + *     +--------+       MQTT        +-------------+       HTTP Hook        +---------+
                              + *     | 设备   | ----------------> | EMQX Broker | --------------------> | 网关    |
                              + *     +--------+                   +-------------+                        +---------+
                              + *         |                              |                                     |
                              + *         | 1. 连接认证                   | 2. 调用 /mqtt/auth                   |
                              + *         | 3. 发布消息                   | 4. 调用 /mqtt/event (上线/下线)       |
                              + *         |                              | 5. 网关订阅 EMQX 消息                 |
                              + *         |                              |                                     |
                              + * 
                              + * + *

                              测试分类: + *

                                + *
                              • 第一部分:模拟设备连接 EMQX Broker,发送 MQTT 消息
                              • + *
                              • 第二部分:模拟 EMQX Server 调用网关 HTTP Hook 接口(认证、事件)
                              • + *
                              + * + *

                              使用步骤: + *

                                + *
                              1. 启动 EMQX Broker(MQTT 端口 1883)
                              2. + *
                              3. 启动 yudao-module-iot-gateway 服务(HTTP 端口 18083)
                              4. + *
                              5. 配置 EMQX HTTP 认证插件指向网关的 /mqtt/auth 接口
                              6. + *
                              7. 配置 EMQX Webhook 插件指向网关的 /mqtt/event 接口
                              8. + *
                              9. 运行测试方法
                              10. + *
                              + * + * @author 芋道源码 + */ +@Slf4j +@Disabled +@SuppressWarnings("HttpUrlsUsage") +public class IotDirectDeviceEmqxProtocolIntegrationTest { + + private static final String SERVER_HOST = "127.0.0.1"; + /** + * EMQX 认证事件 HTTP 接口端口(网关提供给 EMQX Server 调用) + */ + private static final int HTTP_PORT = 18083; + /** + * EMQX Broker MQTT 端口(设备连接 EMQX) + */ + private static final int MQTT_PORT = 1883; + private static final int TIMEOUT_SECONDS = 10; + + private static Vertx vertx; + + // ===================== 编解码器(EMQX 使用 Alink 协议) ===================== + + private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + + // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== + + private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; + private static final String DEVICE_NAME = "small"; + private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; + + @BeforeAll + public static void setUp() { + vertx = Vertx.vertx(); + } + + @AfterAll + public static void tearDown() { + if (vertx != null) { + vertx.close(); + } + } + + // ================================================================================== + // 第一部分:模拟设备连接 EMQX Broker + // ================================================================================== + + /** + * 设备连接测试:模拟设备连接 EMQX Broker + *

                              + * 当设备连接 EMQX 时,EMQX 会自动调用网关的 /mqtt/auth 接口进行认证 + */ + @Test + public void testDeviceConnect() throws Exception { + // 1. 构建认证信息 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + log.info("[testDeviceConnect][认证信息: clientId={}, username={}, password={}]", + authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); + + // 2. 创建客户端并连接 EMQX Broker + MqttClient client = createClient(authInfo); + try { + client.connect(MQTT_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testDeviceConnect][连接 EMQX Broker 成功,客户端 ID: {}]", client.clientId()); + log.info("[testDeviceConnect][EMQX 会自动调用网关的 /mqtt/auth 接口进行认证]"); + log.info("[testDeviceConnect][EMQX 会自动调用网关的 /mqtt/event 接口通知设备上线]"); + } finally { + disconnect(client); + log.info("[testDeviceConnect][EMQX 会自动调用网关的 /mqtt/event 接口通知设备下线]"); + } + } + + /** + * 属性上报测试:设备通过 EMQX Broker 发布属性消息 + *

                              + * 消息流程:设备 -> EMQX Broker -> 网关(订阅 EMQX 消息) + */ + @Test + public void testPropertyPost() throws Exception { + // 1. 连接 EMQX Broker + MqttClient client = connectToEmqx(); + log.info("[testPropertyPost][连接 EMQX Broker 成功]"); + + try { + // 2.1 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), + IotDevicePropertyPostReqDTO.of(MapUtil.builder() + .put("width", 1) + .put("height", "2") + .build())); + + // 2.2 发布消息到 EMQX Broker + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); + publish(client, topic, request); + log.info("[testPropertyPost][属性上报消息已发送到 EMQX Broker]"); + log.info("[testPropertyPost][网关会通过订阅 EMQX 接收此消息]"); + + // 2.3 等待消息处理 + Thread.sleep(2000); + log.info("[testPropertyPost][请检查网关日志确认消息是否被正确处理]"); + } finally { + disconnect(client); + } + } + + /** + * 事件上报测试:设备通过 EMQX Broker 发布事件消息 + *

                              + * 消息流程:设备 -> EMQX Broker -> 网关(订阅 EMQX 消息) + */ + @Test + public void testEventPost() throws Exception { + // 1. 连接 EMQX Broker + MqttClient client = connectToEmqx(); + log.info("[testEventPost][连接 EMQX Broker 成功]"); + + try { + // 2.1 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), + IotDeviceEventPostReqDTO.of( + "eat", + MapUtil.builder().put("rice", 3).build(), + System.currentTimeMillis())); + + // 2.2 发布消息到 EMQX Broker + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); + publish(client, topic, request); + log.info("[testEventPost][事件上报消息已发送到 EMQX Broker]"); + log.info("[testEventPost][网关会通过订阅 EMQX 接收此消息]"); + + // 2.3 等待消息处理 + Thread.sleep(2000); + log.info("[testEventPost][请检查网关日志确认消息是否被正确处理]"); + } finally { + disconnect(client); + } + } + + /** + * 订阅下行消息测试:设备订阅服务端下发的消息 + *

                              + * 消息流程:网关 -> EMQX Broker -> 设备 + */ + @Test + public void testSubscribe() throws Exception { + // 1. 连接 EMQX Broker + MqttClient client = connectToEmqx(); + log.info("[testSubscribe][连接 EMQX Broker 成功]"); + + try { + // 2. 设置消息处理器 + client.publishHandler(message -> log.info("[testSubscribe][收到下行消息: topic={}, payload={}]", + message.topicName(), message.payload().toString())); + + // 3. 订阅下行主题 + String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); + log.info("[testSubscribe][订阅主题: {}]", topic); + subscribe(client, topic); + log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); + log.info("[testSubscribe][网关下发的消息会通过 EMQX Broker 转发给设备]"); + + // 4. 保持连接 30 秒等待消息 + Thread.sleep(30000); + } finally { + disconnect(client); + } + } + + // ================================================================================== + // 第二部分:模拟 EMQX Server 调用网关 HTTP Hook 接口 + // 说明:这些接口是 EMQX Server 自动调用的,这里只是用于单独测试接口功能 + // ================================================================================== + + /** + * 认证接口测试:模拟 EMQX Server 调用 /mqtt/auth 接口 + *

                              + * 注意:正常情况下此接口由 EMQX HTTP 认证插件自动调用,这里只是测试接口本身 + */ + @Test + public void testEmqxAuthHook() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/mqtt/auth", SERVER_HOST, HTTP_PORT); + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + // 1.2 EMQX 认证请求格式 + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("clientid", authInfo.getClientId()) + .put("username", authInfo.getUsername()) + .put("password", authInfo.getPassword()) + .build()); + // 1.3 输出请求 + log.info("[testEmqxAuthHook][模拟 EMQX Server 调用认证接口]"); + log.info("[testEmqxAuthHook][请求 URL: {}]", url); + log.info("[testEmqxAuthHook][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Content-Type", "application/json") + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testEmqxAuthHook][响应状态码: {}]", httpResponse.getStatus()); + log.info("[testEmqxAuthHook][响应体: {}]", httpResponse.body()); + log.info("[testEmqxAuthHook][认证结果: result=allow 表示认证成功, result=deny 表示认证失败]"); + } + } + + /** + * 认证失败测试:模拟 EMQX Server 调用 /mqtt/auth 接口(错误密码) + */ + @Test + public void testEmqxAuthHookFailed() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/mqtt/auth", SERVER_HOST, HTTP_PORT); + // 1.2 使用错误的密码 + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("clientid", PRODUCT_KEY + "." + DEVICE_NAME) + .put("username", DEVICE_NAME + "&" + PRODUCT_KEY) + .put("password", "wrong_password") + .build()); + // 1.3 输出请求 + log.info("[testEmqxAuthHookFailed][模拟 EMQX Server 调用认证接口(错误密码)]"); + log.info("[testEmqxAuthHookFailed][请求 URL: {}]", url); + log.info("[testEmqxAuthHookFailed][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Content-Type", "application/json") + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testEmqxAuthHookFailed][响应状态码: {}]", httpResponse.getStatus()); + log.info("[testEmqxAuthHookFailed][响应体: {}]", httpResponse.body()); + log.info("[testEmqxAuthHookFailed][预期结果: result=deny]"); + } + } + + /** + * 设备上线事件测试:模拟 EMQX Server Webhook 调用 /mqtt/event 接口 + *

                              + * 注意:正常情况下此接口由 EMQX Webhook 插件自动调用,这里只是测试接口本身 + */ + @Test + public void testEmqxClientConnectedHook() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/mqtt/event", SERVER_HOST, HTTP_PORT); + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + // 1.2 EMQX Webhook client.connected 事件格式 + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("event", "client.connected") + .put("clientid", authInfo.getClientId()) + .put("username", authInfo.getUsername()) + .put("peername", "127.0.0.1:12345") + .put("connected_at", System.currentTimeMillis()) + .build()); + // 1.3 输出请求 + log.info("[testEmqxClientConnectedHook][模拟 EMQX Server Webhook 调用设备上线事件]"); + log.info("[testEmqxClientConnectedHook][请求 URL: {}]", url); + log.info("[testEmqxClientConnectedHook][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Content-Type", "application/json") + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testEmqxClientConnectedHook][响应状态码: {}]", httpResponse.getStatus()); + log.info("[testEmqxClientConnectedHook][响应体: {}]", httpResponse.body()); + log.info("[testEmqxClientConnectedHook][预期结果: 状态码 200,设备状态更新为在线]"); + } + } + + /** + * 设备下线事件测试:模拟 EMQX Server Webhook 调用 /mqtt/event 接口 + *

                              + * 注意:正常情况下此接口由 EMQX Webhook 插件自动调用,这里只是测试接口本身 + */ + @Test + public void testEmqxClientDisconnectedHook() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/mqtt/event", SERVER_HOST, HTTP_PORT); + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + // 1.2 EMQX Webhook client.disconnected 事件格式 + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("event", "client.disconnected") + .put("clientid", authInfo.getClientId()) + .put("username", authInfo.getUsername()) + .put("reason", "normal") + .put("disconnected_at", System.currentTimeMillis()) + .build()); + // 1.3 输出请求 + log.info("[testEmqxClientDisconnectedHook][模拟 EMQX Server Webhook 调用设备下线事件]"); + log.info("[testEmqxClientDisconnectedHook][请求 URL: {}]", url); + log.info("[testEmqxClientDisconnectedHook][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Content-Type", "application/json") + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testEmqxClientDisconnectedHook][响应状态码: {}]", httpResponse.getStatus()); + log.info("[testEmqxClientDisconnectedHook][响应体: {}]", httpResponse.body()); + log.info("[testEmqxClientDisconnectedHook][预期结果: 状态码 200,设备状态更新为离线]"); + } + } + + // ===================== 辅助方法 ===================== + + /** + * 创建 MQTT 客户端 + * + * @param authInfo 认证信息 + * @return MQTT 客户端 + */ + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { + MqttClientOptions options = new MqttClientOptions() + .setClientId(authInfo.getClientId()) + .setUsername(authInfo.getUsername()) + .setPassword(authInfo.getPassword()) + .setCleanSession(true) + .setKeepAliveInterval(60); + return MqttClient.create(vertx, options); + } + + /** + * 连接 EMQX Broker 并认证设备 + * + * @return 已认证的 MQTT 客户端 + */ + private MqttClient connectToEmqx() throws Exception { + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + MqttClient client = createClient(authInfo); + client.connect(MQTT_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return client; + } + + /** + * 订阅主题 + * + * @param client MQTT 客户端 + * @param topic 主题 + */ + private void subscribe(MqttClient client, String topic) throws Exception { + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[subscribe][订阅主题成功: {}]", topic); + } + + /** + * 发布消息 + * + * @param client MQTT 客户端 + * @param topic 发布主题 + * @param request 请求消息 + */ + private void publish(MqttClient client, String topic, IotDeviceMessage request) throws Exception { + byte[] payload = CODEC.encode(request); + log.info("[publish][发送消息: topic={}, payload={}]", topic, new String(payload)); + client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[publish][消息发布成功]"); + } + + /** + * 断开连接 + * + * @param client MQTT 客户端 + */ + private void disconnect(MqttClient client) throws Exception { + client.disconnect() + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[disconnect][断开连接成功]"); + } + +} From e2b995d448955285ee340be209e0dc2d1e7a1c75 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 3 Feb 2026 19:59:53 +0800 Subject: [PATCH 26/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91emqx=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=8840%=EF=BC=89?= =?UTF-8?q?=EF=BC=9A=E6=95=B4=E4=BD=93=E4=BB=A3=E7=A0=81=E9=A3=8E=E6=A0=BC?= =?UTF-8?q?=E7=9A=84=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/IotGatewayConfiguration.java | 42 -- .../gateway/config/IotGatewayProperties.java | 209 +------- .../gateway/protocol/IotProtocolManager.java | 13 + .../emqx/IotEmqxAuthEventProtocol.java | 104 ---- .../gateway/protocol/emqx/IotEmqxConfig.java | 225 ++++++++ .../protocol/emqx/IotEmqxProtocol.java | 503 ++++++++++++++++++ .../emqx/IotEmqxUpstreamProtocol.java | 386 -------------- .../downstream/IotEmqxDownstreamHandler.java | 8 +- .../IotEmqxDownstreamSubscriber.java | 17 +- .../upstream/IotEmqxAuthEventHandler.java | 296 +++++++---- .../upstream/IotEmqxUpstreamHandler.java | 9 +- .../iot/gateway/util/IotMqttTopicUtils.java | 32 +- .../src/main/resources/application.yaml | 73 +-- ...rectDeviceEmqxProtocolIntegrationTest.java | 437 --------------- .../gateway/protocol/emqx/package-info.java | 18 + 15 files changed, 1057 insertions(+), 1315 deletions(-) delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotDirectDeviceEmqxProtocolIntegrationTest.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index aa57281e04..3c62c0d221 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -1,23 +1,13 @@ package cn.iocoder.yudao.module.iot.gateway.config; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; -import io.vertx.core.Vertx; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - @Configuration @EnableConfigurationProperties(IotGatewayProperties.class) -@Slf4j public class IotGatewayConfiguration { @Bean @@ -30,36 +20,4 @@ public class IotGatewayConfiguration { return new IotProtocolManager(gatewayProperties); } - /** - * IoT 网关 EMQX 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true") - @Slf4j - public static class EmqxProtocolConfiguration { - - @Bean(name = "emqxVertx", destroyMethod = "close") - public Vertx emqxVertx() { - return Vertx.vertx(); - } - - @Bean - public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties, - @Qualifier("emqxVertx") Vertx emqxVertx) { - return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); - } - - @Bean - public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties, - @Qualifier("emqxVertx") Vertx emqxVertx) { - return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); - } - - @Bean - public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol, - IotMessageBus messageBus) { - return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus); - } - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 8b2618aed2..d91a2f8041 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.config; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; @@ -31,11 +32,6 @@ public class IotGatewayProperties { */ private TokenProperties token; - /** - * 协议配置(旧版,保持兼容) - */ - private ProtocolProperties protocol; - /** * 协议实例列表 */ @@ -78,186 +74,6 @@ public class IotGatewayProperties { } - @Data - public static class ProtocolProperties { - - /** - * EMQX 组件配置 - */ - private EmqxProperties emqx; - - } - - @Data - public static class EmqxProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * HTTP 服务端口(默认:8090) - */ - private Integer httpPort = 8090; - - /** - * MQTT 服务器地址 - */ - @NotEmpty(message = "MQTT 服务器地址不能为空") - private String mqttHost; - - /** - * MQTT 服务器端口(默认:1883) - */ - @NotNull(message = "MQTT 服务器端口不能为空") - private Integer mqttPort = 1883; - - /** - * MQTT 用户名 - */ - @NotEmpty(message = "MQTT 用户名不能为空") - private String mqttUsername; - - /** - * MQTT 密码 - */ - @NotEmpty(message = "MQTT 密码不能为空") - private String mqttPassword; - - /** - * MQTT 客户端的 SSL 开关 - */ - @NotNull(message = "MQTT 是否开启 SSL 不能为空") - private Boolean mqttSsl = false; - - /** - * MQTT 客户端 ID(如果为空,系统将自动生成) - */ - @NotEmpty(message = "MQTT 客户端 ID 不能为空") - private String mqttClientId; - - /** - * MQTT 订阅的主题 - */ - @NotEmpty(message = "MQTT 主题不能为空") - private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics; - - /** - * 默认 QoS 级别 - *

                              - * 0 - 最多一次 - * 1 - 至少一次 - * 2 - 刚好一次 - */ - private Integer mqttQos = 1; - - /** - * 连接超时时间(秒) - */ - private Integer connectTimeoutSeconds = 10; - - /** - * 重连延迟时间(毫秒) - */ - private Long reconnectDelayMs = 5000L; - - /** - * 是否启用 Clean Session (清理会话) - * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。 - * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。 - */ - private Boolean cleanSession = true; - - /** - * 心跳间隔(秒) - * 用于保持连接活性,及时发现网络中断。 - */ - private Integer keepAliveIntervalSeconds = 60; - - /** - * 最大未确认消息队列大小 - * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。 - */ - private Integer maxInflightQueue = 10000; - - /** - * 是否信任所有 SSL 证书 - * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用! - * 在生产环境中,应设置为 false,并配置正确的信任库。 - */ - private Boolean trustAll = false; - - /** - * 遗嘱消息配置 (用于网关异常下线时通知其他系统) - */ - private final Will will = new Will(); - - /** - * 高级 SSL/TLS 配置 (用于生产环境) - */ - private final Ssl sslOptions = new Ssl(); - - /** - * 遗嘱消息 (Last Will and Testament) - */ - @Data - public static class Will { - - /** - * 是否启用遗嘱消息 - */ - private boolean enabled = false; - /** - * 遗嘱消息主题 - */ - private String topic; - /** - * 遗嘱消息内容 - */ - private String payload; - /** - * 遗嘱消息 QoS 等级 - */ - private Integer qos = 1; - /** - * 遗嘱消息是否作为保留消息发布 - */ - private boolean retain = true; - - } - - /** - * 高级 SSL/TLS 配置 - */ - @Data - public static class Ssl { - - /** - * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks - * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。 - */ - private String keyStorePath; - /** - * 密钥库密码 - */ - private String keyStorePassword; - /** - * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks - * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。 - */ - private String trustStorePath; - /** - * 信任库密码 - */ - private String trustStorePassword; - - } - - } - - // NOTE:暂未统一为 ProtocolProperties,待协议改造完成再调整 /** * 协议实例配置 */ @@ -283,6 +99,10 @@ public class IotGatewayProperties { private String protocol; /** * 服务端口 + *

                              + * 不同协议含义不同: + * 1. TCP/UDP/HTTP/WebSocket/MQTT/CoAP:对应网关自身监听的服务端口 + * 2. EMQX:对应网关提供给 EMQX 回调的 HTTP Hook 端口(/mqtt/auth、/mqtt/acl、/mqtt/event) */ @NotNull(message = "服务端口不能为空") private Integer port; @@ -292,7 +112,7 @@ public class IotGatewayProperties { * @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum * * 为什么是可选的呢? - * 1. {@link IotProtocolTypeEnum#HTTP}、${@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式 + * 1. {@link IotProtocolTypeEnum#HTTP}、{@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式 * 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析 */ private String serialize; @@ -304,13 +124,17 @@ public class IotGatewayProperties { */ @Valid private IotHttpConfig http; + /** + * WebSocket 协议配置 + */ + @Valid + private IotWebSocketConfig websocket; /** * TCP 协议配置 */ @Valid private IotTcpConfig tcp; - /** * UDP 协议配置 */ @@ -323,17 +147,16 @@ public class IotGatewayProperties { @Valid private IotCoapConfig coap; - /** - * WebSocket 协议配置 - */ - @Valid - private IotWebSocketConfig websocket; - /** * MQTT 协议配置 */ @Valid private IotMqttConfig mqtt; + /** + * EMQX 协议配置 + */ + @Valid + private IotEmqxConfig emqx; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index b97da74a72..5d1b6c6d7f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -5,6 +5,7 @@ import cn.hutool.core.util.BooleanUtil; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; @@ -109,6 +110,8 @@ public class IotProtocolManager implements SmartLifecycle { return createWebSocketProtocol(config); case MQTT: return createMqttProtocol(config); + case EMQX: + return createEmqxProtocol(config); default: throw new IllegalArgumentException(String.format( "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); @@ -175,4 +178,14 @@ public class IotProtocolManager implements SmartLifecycle { return new IotMqttProtocol(config); } + /** + * 创建 EMQX 协议实例 + * + * @param config 协议实例配置 + * @return EMQX 协议实例 + */ + private IotEmqxProtocol createEmqxProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + return new IotEmqxProtocol(config); + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java deleted file mode 100644 index 5d3b5e3c00..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java +++ /dev/null @@ -1,104 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; - -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler; -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 EMQX 认证事件协议服务 - *

                              - * 为 EMQX 提供 HTTP 接口服务,包括: - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 - * - * @author 芋道源码 - */ -@Slf4j -public class IotEmqxAuthEventProtocol { - - private final IotGatewayProperties.EmqxProperties emqxProperties; - - private final String serverId; - - private final Vertx vertx; - - private HttpServer httpServer; - - public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties, - Vertx vertx) { - this.emqxProperties = emqxProperties; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); - } - - @PostConstruct - public void start() { - try { - startHttpServer(); - log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 EMQX 认证事件协议服务启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - stopHttpServer(); - log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]"); - } - - /** - * 启动 HTTP 服务器 - */ - private void startHttpServer() { - int port = emqxProperties.getHttpPort(); - - // 1. 创建路由 - Router router = Router.router(vertx); - router.route().handler(BodyHandler.create()); - - // 2. 创建处理器,传入 serverId - IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId); - router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); - router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); - // TODO @haohao:/mqtt/acl 需要处理么? - // TODO @芋艿:已在 EMQX 处理,如果是“设备直连”模式需要处理 - - // 3. 启动 HTTP 服务器 - try { - httpServer = vertx.createHttpServer() - .requestHandler(router) - .listen(port) - .result(); - } catch (Exception e) { - log.error("[startHttpServer][HTTP 服务器启动失败, 端口: {}]", port, e); - throw e; - } - } - - /** - * 停止 HTTP 服务器 - */ - private void stopHttpServer() { - if (httpServer == null) { - return; - } - - try { - httpServer.close().result(); - log.info("[stopHttpServer][HTTP 服务器已停止]"); - } catch (Exception e) { - log.error("[stopHttpServer][HTTP 服务器停止失败]", e); - } - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java new file mode 100644 index 0000000000..bc039fe5c4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java @@ -0,0 +1,225 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * IoT EMQX 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotEmqxConfig { + + // ========== MQTT Client 配置(连接 EMQX Broker) ========== + + /** + * MQTT 服务器地址 + */ + @NotEmpty(message = "MQTT 服务器地址不能为空") + private String mqttHost; + + /** + * MQTT 服务器端口(默认:1883) + */ + @NotNull(message = "MQTT 服务器端口不能为空") + private Integer mqttPort = 1883; + + /** + * MQTT 用户名 + */ + @NotEmpty(message = "MQTT 用户名不能为空") + private String mqttUsername; + + /** + * MQTT 密码 + */ + @NotEmpty(message = "MQTT 密码不能为空") + private String mqttPassword; + + /** + * MQTT 客户端的 SSL 开关 + */ + @NotNull(message = "MQTT 是否开启 SSL 不能为空") + private Boolean mqttSsl = false; + + /** + * MQTT 客户端 ID + */ + @NotEmpty(message = "MQTT 客户端 ID 不能为空") + private String mqttClientId; + + /** + * MQTT 订阅的主题 + */ + @NotEmpty(message = "MQTT 主题不能为空") + private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics; + + /** + * 默认 QoS 级别 + *

                              + * 0 - 最多一次 + * 1 - 至少一次 + * 2 - 刚好一次 + */ + @NotNull(message = "MQTT QoS 不能为空") + @Min(value = 0, message = "MQTT QoS 不能小于 0") + @Max(value = 2, message = "MQTT QoS 不能大于 2") + private Integer mqttQos = 1; + + /** + * 连接超时时间(秒) + */ + @NotNull(message = "连接超时时间不能为空") + @Min(value = 1, message = "连接超时时间不能小于 1 秒") + private Integer connectTimeoutSeconds = 10; + + /** + * 重连延迟时间(毫秒) + */ + @NotNull(message = "重连延迟时间不能为空") + @Min(value = 0, message = "重连延迟时间不能小于 0 毫秒") + private Long reconnectDelayMs = 5000L; + + /** + * 是否启用 Clean Session (清理会话) + * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。 + * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。 + */ + @NotNull(message = "是否启用 Clean Session 不能为空") + private Boolean cleanSession = true; + + /** + * 心跳间隔(秒) + * 用于保持连接活性,及时发现网络中断。 + */ + @NotNull(message = "心跳间隔不能为空") + @Min(value = 1, message = "心跳间隔不能小于 1 秒") + private Integer keepAliveIntervalSeconds = 60; + + /** + * 最大未确认消息队列大小 + * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。 + */ + @NotNull(message = "最大未确认消息队列大小不能为空") + @Min(value = 1, message = "最大未确认消息队列大小不能小于 1") + private Integer maxInflightQueue = 10000; + + /** + * 是否信任所有 SSL 证书 + * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用! + * 在生产环境中,应设置为 false,并配置正确的信任库。 + */ + @NotNull(message = "是否信任所有 SSL 证书不能为空") + private Boolean trustAll = false; + + // ========== MQTT Will / SSL 高级配置 ========== + + /** + * 遗嘱消息配置 (用于网关异常下线时通知其他系统) + */ + @Valid + private Will will = new Will(); + + /** + * 高级 SSL/TLS 配置 (用于生产环境) + */ + @Valid + private Ssl sslOptions = new Ssl(); + + // ========== HTTP Hook 配置(网关提供给 EMQX 调用) ========== + + /** + * HTTP Hook 服务配置(用于 /mqtt/auth、/mqtt/event) + */ + @Valid + private Http http = new Http(); + + /** + * 遗嘱消息 (Last Will and Testament) + */ + @Data + public static class Will { + + /** + * 是否启用遗嘱消息 + */ + private boolean enabled = false; + /** + * 遗嘱消息主题 + */ + private String topic; + /** + * 遗嘱消息内容 + */ + private String payload; + /** + * 遗嘱消息 QoS 等级 + */ + @Min(value = 0, message = "遗嘱消息 QoS 不能小于 0") + @Max(value = 2, message = "遗嘱消息 QoS 不能大于 2") + private Integer qos = 1; + /** + * 遗嘱消息是否作为保留消息发布 + */ + private boolean retain = true; + + } + + /** + * 高级 SSL/TLS 配置 + */ + @Data + public static class Ssl { + + /** + * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。 + */ + private String keyStorePath; + /** + * 密钥库密码 + */ + private String keyStorePassword; + /** + * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks + * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。 + */ + private String trustStorePath; + /** + * 信任库密码 + */ + private String trustStorePassword; + + } + + /** + * HTTP Hook 服务 SSL 配置 + */ + @Data + public static class Http { + + /** + * 是否启用 SSL + */ + private Boolean sslEnabled = false; + + /** + * SSL 证书路径 + */ + private String sslCertPath; + + /** + * SSL 私钥路径 + */ + private String sslKeyPath; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java new file mode 100644 index 0000000000..226e421024 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java @@ -0,0 +1,503 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.JksOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.MqttClientOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * IoT 网关 EMQX 协议实现: + *

                              + * 1. 提供 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event)给 EMQX 调用 + * 2. 通过 MQTT Client 订阅设备上行消息,并发布下行消息到 Broker + * + * @author 芋道源码 + */ +@Slf4j +public class IotEmqxProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolInstanceProperties properties; + /** + * EMQX 配置 + */ + private final IotEmqxConfig emqxConfig; + /** + * 服务器 ID + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * HTTP Hook 服务器 + */ + private HttpServer httpServer; + + /** + * MQTT Client + */ + private volatile MqttClient mqttClient; + /** + * MQTT 重连定时器 ID + */ + private volatile Long reconnectTimerId; + + /** + * 上行消息处理器 + */ + private final IotEmqxUpstreamHandler upstreamHandler; + + /** + * 下行消息订阅者 + */ + private final IotEmqxDownstreamSubscriber downstreamSubscriber; + + public IotEmqxProtocol(ProtocolInstanceProperties properties) { + Assert.notNull(properties, "协议实例配置不能为空"); + Assert.notNull(properties.getEmqx(), "EMQX 协议配置(emqx)不能为空"); + this.properties = properties; + this.emqxConfig = properties.getEmqx(); + Assert.notNull(emqxConfig.getConnectTimeoutSeconds(), + "MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空"); + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + this.upstreamHandler = new IotEmqxUpstreamHandler(serverId); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.EMQX; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT EMQX 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + + try { + // 1.2 启动 HTTP Hook 服务 + startHttpServer(); + + // 1.3 启动 MQTT Client + startMqttClient(); + running = true; + log.info("[start][IoT EMQX 协议 {} 启动成功,hookPort:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e); + // 启动失败时,关闭资源 + stop0(); + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + stop0(); + } + + private void stop0() { + // 1. 停止下行消息订阅者 + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + + // 2.1 先置为 false:避免 closeHandler 触发重连 + running = false; + stopMqttClientReconnectChecker(); + // 2.2 停止 MQTT Client + stopMqttClient(); + + // 2.3 停止 HTTP Hook 服务 + stopHttpServer(); + + // 2.4 关闭 Vertx + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT EMQX 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT EMQX 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + + log.info("[stop][IoT EMQX 协议 {} 已停止]", getId()); + } + + // ======================================= HTTP Hook Server ======================================= + + /** + * 启动 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event) + */ + private void startHttpServer() { + // 1. 创建路由 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); + + // 2. 创建处理器 + IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId); + router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); + router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl); + router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); + + // 3. 启动 HTTP Server(支持 HTTPS) + IotEmqxConfig.Http httpConfig = emqxConfig.getHttp(); + HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); + if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) { + Assert.notBlank(httpConfig.getSslCertPath(), "EMQX HTTP SSL 证书路径(emqx.http.ssl-cert-path)不能为空"); + Assert.notBlank(httpConfig.getSslKeyPath(), "EMQX HTTP SSL 私钥路径(emqx.http.ssl-key-path)不能为空"); + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(httpConfig.getSslKeyPath()) + .setCertPath(httpConfig.getSslCertPath()); + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + try { + httpServer = vertx.createHttpServer(options) + .requestHandler(router) + .listen() + .result(); + log.info("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动成功, port: {}, ssl: {}]", + getId(), properties.getPort(), httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())); + } catch (Exception e) { + log.error("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动失败, port: {}]", getId(), properties.getPort(), e); + throw e; + } + } + + private void stopHttpServer() { + if (httpServer == null) { + return; + } + try { + httpServer.close().result(); + log.info("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务已停止]", getId()); + } catch (Exception e) { + log.error("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务停止失败]", getId(), e); + } finally { + httpServer = null; + } + } + + // ======================================= MQTT Client ====================================== + + private void startMqttClient() { + // 1.1 创建 MQTT Client + MqttClient client = createMqttClient(); + this.mqttClient = client; + // 1.2 连接 MQTT Broker + if (!connectMqttClient(client)) { + throw new RuntimeException("MQTT Client 启动失败: 连接 Broker 失败"); + } + + // 2. 启动定时重连检查 + startMqttClientReconnectChecker(); + } + + private void stopMqttClient() { + MqttClient client = this.mqttClient; + if (client == null || !client.isConnected()) { + return; + } + this.mqttClient = null; + + // 1. 批量取消订阅 + List topicList = emqxConfig.getMqttTopics(); + if (CollUtil.isNotEmpty(topicList)) { + try { + client.unsubscribe(topicList).toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); + } catch (Exception e) { + log.warn("[stopMqttClient][IoT EMQX 协议 {} 取消订阅异常]", getId(), e); + } + } + + // 2. 断开 MQTT 连接 + try { + client.disconnect().toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); + } catch (Exception e) { + log.warn("[stopMqttClient][IoT EMQX 协议 {} 断开连接异常]", getId(), e); + } + } + + // ======================================= MQTT 基础方法 ====================================== + + /** + * 创建 MQTT 客户端 + * + * @return 新创建的 MqttClient + */ + private MqttClient createMqttClient() { + // 1.1 基础配置 + MqttClientOptions options = new MqttClientOptions() + .setClientId(emqxConfig.getMqttClientId()) + .setUsername(emqxConfig.getMqttUsername()) + .setPassword(emqxConfig.getMqttPassword()) + .setSsl(emqxConfig.getMqttSsl()) + .setCleanSession(emqxConfig.getCleanSession()) + .setKeepAliveInterval(emqxConfig.getKeepAliveIntervalSeconds()) + .setMaxInflightQueue(emqxConfig.getMaxInflightQueue()); + options.setConnectTimeout(emqxConfig.getConnectTimeoutSeconds() * 1000); // Vert.x 需要毫秒 + options.setTrustAll(emqxConfig.getTrustAll()); + // 1.2 配置遗嘱消息 + IotEmqxConfig.Will will = emqxConfig.getWill(); + if (will.isEnabled()) { + Assert.notBlank(will.getTopic(), "遗嘱消息主题(emqx.will.topic)不能为空"); + Assert.notNull(will.getPayload(), "遗嘱消息内容(emqx.will.payload)不能为空"); + options.setWillFlag(true) + .setWillTopic(will.getTopic()) + .setWillMessageBytes(Buffer.buffer(will.getPayload())) + .setWillQoS(will.getQos()) + .setWillRetain(will.isRetain()); + } + // 1.3 配置高级 SSL/TLS(仅在启用 SSL 且不信任所有证书时生效) + if (Boolean.TRUE.equals(emqxConfig.getMqttSsl()) && !Boolean.TRUE.equals(emqxConfig.getTrustAll())) { + IotEmqxConfig.Ssl sslOptions = emqxConfig.getSslOptions(); + if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) { + options.setTrustStoreOptions(new JksOptions() + .setPath(sslOptions.getTrustStorePath()) + .setPassword(sslOptions.getTrustStorePassword())); + } + if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) { + options.setKeyStoreOptions(new JksOptions() + .setPath(sslOptions.getKeyStorePath()) + .setPassword(sslOptions.getKeyStorePassword())); + } + } + + // 2. 创建客户端 + return MqttClient.create(vertx, options); + } + + /** + * 连接 MQTT Broker(同步等待) + * + * @param client MQTT 客户端 + * @return 连接成功返回 true,失败返回 false + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private synchronized boolean connectMqttClient(MqttClient client) { + String host = emqxConfig.getMqttHost(); + int port = emqxConfig.getMqttPort(); + int timeoutSeconds = emqxConfig.getConnectTimeoutSeconds(); + try { + // 1. 连接 Broker + client.connect(port, host).toCompletionStage().toCompletableFuture() + .get(timeoutSeconds, TimeUnit.SECONDS); + log.info("[connectMqttClient][IoT EMQX 协议 {} 连接成功, host: {}, port: {}]", + getId(), host, port); + + // 2. 设置处理器 + setupMqttClientHandlers(client); + subscribeMqttClientTopics(client); + return true; + } catch (Exception e) { + log.error("[connectMqttClient][IoT EMQX 协议 {} 连接发生异常]", getId(), e); + return false; + } + } + + /** + * 关闭 MQTT 客户端 + */ + private void closeMqttClient() { + MqttClient oldClient = this.mqttClient; + if (oldClient == null || !oldClient.isConnected()) { + return; + } + this.mqttClient = null; + try { + oldClient.disconnect().toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); + } catch (Exception ignored) { + } + } + + // ======================================= MQTT 重连机制 ====================================== + + /** + * 启动 MQTT Client 周期性重连检查器 + */ + private void startMqttClientReconnectChecker() { + long interval = emqxConfig.getReconnectDelayMs(); + this.reconnectTimerId = vertx.setPeriodic(interval, timerId -> { + if (!running) { + return; + } + if (mqttClient != null && mqttClient.isConnected()) { + return; + } + log.info("[startMqttClientReconnectChecker][IoT EMQX 协议 {} 检测到断开,尝试重连]", getId()); + tryReconnectMqttClient(); + }); + } + + /** + * 停止 MQTT Client 重连检查器 + */ + private void stopMqttClientReconnectChecker() { + if (reconnectTimerId != null && vertx != null) { + try { + vertx.cancelTimer(reconnectTimerId); + } catch (Exception ignored) { + } + reconnectTimerId = null; + } + } + + /** + * 尝试重连 MQTT Client + */ + private synchronized void tryReconnectMqttClient() { + // 1. 前置检查 + if (!running) { + return; + } + if (mqttClient != null && mqttClient.isConnected()) { + return; + } + + log.info("[tryReconnectMqttClient][IoT EMQX 协议 {} 开始重连]", getId()); + try { + // 2. 关闭旧客户端 + closeMqttClient(); + + // 3.1 创建新客户端 + MqttClient client = createMqttClient(); + this.mqttClient = client; + // 3.2 连接(失败只打印日志,等下次定时) + if (!connectMqttClient(client)) { + log.warn("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连失败,等待下次重试]", getId()); + } + } catch (Exception e) { + log.error("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连异常]", getId(), e); + } + } + + // ======================================= MQTT Handler ====================================== + + /** + * 设置 MQTT Client 事件处理器 + */ + private void setupMqttClientHandlers(MqttClient client) { + // 1. 断开重连监听 + client.closeHandler(closeEvent -> { + if (!running) { + return; + } + log.warn("[setupMqttClientHandlers][IoT EMQX 协议 {} 连接断开,立即尝试重连]", getId()); + vertx.runOnContext(v -> tryReconnectMqttClient()); + }); + + // 2. 异常处理 + client.exceptionHandler(exception -> + log.error("[setupMqttClientHandlers][IoT EMQX 协议 {} MQTT Client 异常]", getId(), exception)); + + // 3. 上行消息处理 + client.publishHandler(upstreamHandler::handle); + } + + /** + * 订阅 MQTT Client 主题(同步等待) + */ + private void subscribeMqttClientTopics(MqttClient client) { + List topicList = emqxConfig.getMqttTopics(); + if (!client.isConnected()) { + log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} MQTT Client 未连接, 跳过订阅]", getId()); + return; + } + if (CollUtil.isEmpty(topicList)) { + log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} 未配置订阅主题, 跳过订阅]", getId()); + return; + } + // 执行订阅 + Map topics = convertMap(emqxConfig.getMqttTopics(), topic -> topic, + topic -> emqxConfig.getMqttQos()); + try { + client.subscribe(topics).toCompletionStage().toCompletableFuture() + .get(10, TimeUnit.SECONDS); + log.info("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅成功, 共 {} 个主题]", getId(), topicList.size()); + } catch (Exception e) { + log.error("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅失败]", getId(), e); + } + } + + /** + * 发布消息到 MQTT Broker + * + * @param topic 主题 + * @param payload 消息内容 + */ + public void publishMessage(String topic, byte[] payload) { + if (mqttClient == null || !mqttClient.isConnected()) { + log.warn("[publishMessage][IoT EMQX 协议 {} MQTT Client 未连接, 无法发布消息]", getId()); + return; + } + MqttQoS qos = MqttQoS.valueOf(emqxConfig.getMqttQos()); + mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java deleted file mode 100644 index 5ebaa1f01e..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java +++ /dev/null @@ -1,386 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.net.JksOptions; -import io.vertx.mqtt.MqttClient; -import io.vertx.mqtt.MqttClientOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * IoT 网关 EMQX 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotEmqxUpstreamProtocol implements IotProtocol { - - private static final String ID = "emqx"; - - private final IotGatewayProperties.EmqxProperties emqxProperties; - - private volatile boolean running = false; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private MqttClient mqttClient; - - private IotEmqxUpstreamHandler upstreamHandler; - - public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties, - Vertx vertx) { - this.emqxProperties = emqxProperties; - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); - this.vertx = vertx; - } - - @Override - public String getId() { - return ID; - } - - @Override - public IotProtocolTypeEnum getType() { - return IotProtocolTypeEnum.EMQX; - } - - @Override - @PostConstruct - public void start() { - if (running) { - return; - } - - try { - // 1. 启动 MQTT 客户端 - startMqttClient(); - - // 2. 标记服务为运行状态 - running = true; - log.info("[start][IoT 网关 EMQX 协议启动成功]"); - } catch (Exception e) { - log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e); - stop(); - - // 异步关闭应用 - Thread shutdownThread = new Thread(() -> { - try { - // 确保日志输出完成,使用更优雅的方式 - log.error("[start][由于 MQTT 连接失败,正在关闭应用]"); - // 等待日志输出完成 - Thread.sleep(1000); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - log.warn("[start][应用关闭被中断]"); - } - System.exit(1); - }); - shutdownThread.setDaemon(true); - shutdownThread.setName("emergency-shutdown"); - shutdownThread.start(); - - throw e; - } - } - - @Override - @PreDestroy - public void stop() { - if (!running) { - return; - } - - // 1. 停止 MQTT 客户端 - stopMqttClient(); - - // 2. 标记服务为停止状态 - running = false; - log.info("[stop][IoT 网关 MQTT 协议服务已停止]"); - } - - @Override - public boolean isRunning() { - return running; - } - - /** - * 启动 MQTT 客户端 - */ - private void startMqttClient() { - try { - // 1. 初始化消息处理器 - this.upstreamHandler = new IotEmqxUpstreamHandler(this); - - // 2. 创建 MQTT 客户端 - createMqttClient(); - - // 3. 同步连接 MQTT Broker - connectMqttSync(); - } catch (Exception e) { - log.error("[startMqttClient][MQTT 客户端启动失败]", e); - throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e); - } - } - - /** - * 同步连接 MQTT Broker - */ - private void connectMqttSync() { - String host = emqxProperties.getMqttHost(); - int port = emqxProperties.getMqttPort(); - // 1. 连接 MQTT Broker - CountDownLatch latch = new CountDownLatch(1); - AtomicBoolean success = new AtomicBoolean(false); - mqttClient.connect(port, host, connectResult -> { - if (connectResult.succeeded()) { - log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port); - setupMqttHandlers(); - subscribeToTopics(); - success.set(true); - } else { - log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]", - host, port, connectResult.cause()); - } - latch.countDown(); - }); - - // 2. 等待连接结果 - try { - // 应用层超时控制:防止启动过程无限阻塞,与MQTT客户端的网络超时是不同层次的控制 - boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS); - if (!awaitResult) { - log.error("[connectMqttSync][等待连接结果超时]"); - throw new RuntimeException("连接 MQTT Broker 超时"); - } - if (!success.get()) { - throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", host, port)); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("[connectMqttSync][等待连接结果被中断]", e); - throw new RuntimeException("连接 MQTT Broker 被中断", e); - } - } - - /** - * 异步连接 MQTT Broker - */ - private void connectMqttAsync() { - String host = emqxProperties.getMqttHost(); - int port = emqxProperties.getMqttPort(); - mqttClient.connect(port, host, connectResult -> { - if (connectResult.succeeded()) { - log.info("[connectMqttAsync][MQTT 客户端重连成功]"); - setupMqttHandlers(); - subscribeToTopics(); - } else { - log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]", - host, port, connectResult.cause()); - log.warn("[connectMqttAsync][重连失败,将再次尝试]"); - reconnectWithDelay(); - } - }); - } - - /** - * 延迟重连 - */ - private void reconnectWithDelay() { - if (!running) { - return; - } - if (mqttClient != null && mqttClient.isConnected()) { - return; - } - - long delay = emqxProperties.getReconnectDelayMs(); - log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay); - vertx.setTimer(delay, timerId -> { - if (!running) { - return; - } - if (mqttClient != null && mqttClient.isConnected()) { - return; - } - - log.info("[reconnectWithDelay][开始重连 MQTT Broker]"); - try { - createMqttClient(); - connectMqttAsync(); - } catch (Exception e) { - log.error("[reconnectWithDelay][重连过程中发生异常]", e); - vertx.setTimer(delay, t -> reconnectWithDelay()); - } - }); - } - - /** - * 停止 MQTT 客户端 - */ - private void stopMqttClient() { - if (mqttClient == null) { - return; - } - try { - if (mqttClient.isConnected()) { - // 1. 取消订阅所有主题 - List topicList = emqxProperties.getMqttTopics(); - for (String topic : topicList) { - try { - mqttClient.unsubscribe(topic); - } catch (Exception e) { - log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e); - } - } - - // 2. 断开 MQTT 客户端连接 - try { - CountDownLatch disconnectLatch = new CountDownLatch(1); - mqttClient.disconnect(ar -> disconnectLatch.countDown()); - if (!disconnectLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)) { - log.warn("[stopMqttClient][断开 MQTT 连接超时]"); - } - } catch (Exception e) { - log.warn("[stopMqttClient][关闭 MQTT 客户端异常]", e); - } - } - } catch (Exception e) { - log.warn("[stopMqttClient][停止 MQTT 客户端过程中发生异常]", e); - } finally { - mqttClient = null; - } - } - - /** - * 创建 MQTT 客户端 - */ - private void createMqttClient() { - // 1.1 创建基础配置 - MqttClientOptions options = (MqttClientOptions) new MqttClientOptions() - .setClientId(emqxProperties.getMqttClientId()) - .setUsername(emqxProperties.getMqttUsername()) - .setPassword(emqxProperties.getMqttPassword()) - .setSsl(emqxProperties.getMqttSsl()) - .setCleanSession(emqxProperties.getCleanSession()) - .setKeepAliveInterval(emqxProperties.getKeepAliveIntervalSeconds()) - .setMaxInflightQueue(emqxProperties.getMaxInflightQueue()) - .setConnectTimeout(emqxProperties.getConnectTimeoutSeconds() * 1000) // Vert.x 需要毫秒 - .setTrustAll(emqxProperties.getTrustAll()); - // 1.2 配置遗嘱消息 - IotGatewayProperties.EmqxProperties.Will will = emqxProperties.getWill(); - if (will.isEnabled()) { - Assert.notBlank(will.getTopic(), "遗嘱消息主题(will.topic)不能为空"); - Assert.notNull(will.getPayload(), "遗嘱消息内容(will.payload)不能为空"); - options.setWillFlag(true) - .setWillTopic(will.getTopic()) - .setWillMessageBytes(Buffer.buffer(will.getPayload())) - .setWillQoS(will.getQos()) - .setWillRetain(will.isRetain()); - } - // 1.3 配置高级 SSL/TLS (仅在启用 SSL 且不信任所有证书时生效) - if (Boolean.TRUE.equals(emqxProperties.getMqttSsl()) && !Boolean.TRUE.equals(emqxProperties.getTrustAll())) { - IotGatewayProperties.EmqxProperties.Ssl sslOptions = emqxProperties.getSslOptions(); - if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) { - options.setTrustStoreOptions(new JksOptions() - .setPath(sslOptions.getTrustStorePath()) - .setPassword(sslOptions.getTrustStorePassword())); - } - if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) { - options.setKeyStoreOptions(new JksOptions() - .setPath(sslOptions.getKeyStorePath()) - .setPassword(sslOptions.getKeyStorePassword())); - } - } - // 1.4 安全警告日志 - if (Boolean.TRUE.equals(emqxProperties.getTrustAll())) { - log.warn("[createMqttClient][安全警告:当前配置信任所有 SSL 证书(trustAll=true),这在生产环境中存在严重安全风险!]"); - } - - // 2. 创建客户端实例 - this.mqttClient = MqttClient.create(vertx, options); - } - - /** - * 设置 MQTT 处理器 - */ - private void setupMqttHandlers() { - // 1. 设置断开重连监听器 - mqttClient.closeHandler(closeEvent -> { - if (!running) { - return; - } - log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); - reconnectWithDelay(); - }); - - // 2. 设置异常处理器 - mqttClient.exceptionHandler(exception -> - log.error("[exceptionHandler][MQTT 客户端异常]", exception)); - - // 3. 设置消息处理器 - mqttClient.publishHandler(upstreamHandler::handle); - } - - /** - * 订阅设备上行消息主题 - */ - private void subscribeToTopics() { - // 1. 校验 MQTT 客户端是否连接 - List topicList = emqxProperties.getMqttTopics(); - if (mqttClient == null || !mqttClient.isConnected()) { - log.warn("[subscribeToTopics][MQTT 客户端未连接, 跳过订阅]"); - return; - } - - // 2. 批量订阅所有主题 - Map topics = new HashMap<>(); - int qos = emqxProperties.getMqttQos(); - for (String topic : topicList) { - topics.put(topic, qos); - } - mqttClient.subscribe(topics, subscribeResult -> { - if (subscribeResult.succeeded()) { - log.info("[subscribeToTopics][订阅主题成功, 共 {} 个主题]", topicList.size()); - } else { - log.error("[subscribeToTopics][订阅主题失败, 共 {} 个主题, 原因: {}]", - topicList.size(), subscribeResult.cause().getMessage(), subscribeResult.cause()); - } - }); - } - - /** - * 发布消息到 MQTT Broker - * - * @param topic 主题 - * @param payload 消息内容 - */ - public void publishMessage(String topic, byte[] payload) { - if (mqttClient == null || !mqttClient.isConnected()) { - log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]"); - return; - } - MqttQoS qos = MqttQoS.valueOf(emqxProperties.getMqttQos()); - mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java index a05fd1120a..77f777cafa 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java @@ -5,7 +5,7 @@ import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; @@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class IotEmqxDownstreamHandler { - private final IotEmqxUpstreamProtocol protocol; + private final IotEmqxProtocol protocol; private final IotDeviceService deviceService; private final IotDeviceMessageService deviceMessageService; - public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) { + public IotEmqxDownstreamHandler(IotEmqxProtocol protocol) { this.protocol = protocol; this.deviceService = SpringUtil.getBean(IotDeviceService.class); this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); @@ -74,4 +74,4 @@ public class IotEmqxDownstreamHandler { return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply); } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java index bcce471987..55aaaac69c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java @@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import lombok.extern.slf4j.Slf4j; /** @@ -18,22 +16,11 @@ public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber private final IotEmqxDownstreamHandler downstreamHandler; - public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) { + public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) { super(protocol, messageBus); this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); } - @PostConstruct - public void startSubscriber() { - // EMQX 模式下,由 Spring 管理 Bean 生命周期;需要显式启动订阅者,才能从消息总线消费下行消息并发布到 Broker - start(); - } - - @PreDestroy - public void stopSubscriber() { - stop(); - } - @Override protected void handleMessage(IotDeviceMessage message) { downstreamHandler.handle(message); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java index ae548cc4b6..0ba250cb1a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java @@ -10,16 +10,20 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import lombok.extern.slf4j.Slf4j; +import java.util.Locale; + /** * IoT 网关 EMQX 认证事件处理器 *

                              * 为 EMQX 提供 HTTP 接口服务,包括: - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 + * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)} + * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)} + * 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)} * * @author 芋道源码 */ @@ -45,15 +49,17 @@ public class IotEmqxAuthEventHandler { private static final String RESULT_IGNORE = "ignore"; /** - * EMQX 事件类型常量 + * EMQX 事件类型常量 - 客户端连接 */ private static final String EVENT_CLIENT_CONNECTED = "client.connected"; + /** + * EMQX 事件类型常量 - 客户端断开连接 + */ private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected"; private final String serverId; private final IotDeviceMessageService deviceMessageService; - private final IotDeviceCommonApi deviceApi; public IotEmqxAuthEventHandler(String serverId) { @@ -62,13 +68,16 @@ public class IotEmqxAuthEventHandler { this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); } + // ========== 认证处理 ========== + /** * EMQX 认证接口 */ public void handleAuth(RoutingContext context) { + JsonObject body = null; try { // 1. 参数校验 - JsonObject body = parseRequestBody(context); + body = parseRequestBody(context); if (body == null) { return; } @@ -91,11 +100,179 @@ public class IotEmqxAuthEventHandler { sendAuthResponse(context, RESULT_DENY); } } catch (Exception e) { - log.error("[handleAuth][设备认证异常]", e); + log.error("[handleAuth][设备认证异常][body={}]", body, e); sendAuthResponse(context, RESULT_IGNORE); } } + /** + * 解析认证接口请求体 + *

                              + * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段) + * + * @param context 路由上下文 + * @return 请求体JSON对象,解析失败时返回null + */ + private JsonObject parseRequestBody(RoutingContext context) { + try { + JsonObject body = context.body().asJsonObject(); + if (body == null) { + log.info("[parseRequestBody][请求体为空]"); + sendAuthResponse(context, RESULT_IGNORE); + return null; + } + return body; + } catch (Exception e) { + log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e); + sendAuthResponse(context, RESULT_IGNORE); + return null; + } + } + + /** + * 执行设备认证 + * + * @param clientId 客户端ID + * @param username 用户名 + * @param password 密码 + * @return 认证是否成功 + */ + private boolean handleDeviceAuth(String clientId, String username, String password) { + try { + CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() + .setClientId(clientId).setUsername(username).setPassword(password)); + result.checkError(); + return BooleanUtil.isTrue(result.getData()); + } catch (Exception e) { + log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e); + throw e; + } + } + + /** + * 发送 EMQX 认证响应 + * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应 + * + * @param context 路由上下文 + * @param result 认证结果:allow、deny、ignore + */ + private void sendAuthResponse(RoutingContext context, String result) { + // 构建符合 EMQX 官方规范的响应 + JsonObject response = new JsonObject() + .put("result", result) + .put("is_superuser", false); + // 可以根据业务需求添加客户端属性 + // response.put("client_attrs", new JsonObject().put("role", "device")); + // 可以添加认证过期时间(可选) + // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); + + // 回复响应 + context.response() + .setStatusCode(SUCCESS_STATUS_CODE) + .putHeader("Content-Type", "application/json; charset=utf-8") + .end(response.encode()); + } + + // ========== ACL 处理 ========== + + /** + * EMQX ACL 接口 + *

                              + * 用于 EMQX 的 HTTP ACL 插件校验设备的 publish/subscribe 权限。 + * 若请求参数无法识别,则返回 ignore 交给 EMQX 自身 ACL 规则处理。 + */ + public void handleAcl(RoutingContext context) { + JsonObject body = null; + try { + // 1.1 解析请求体 + body = parseRequestBody(context); + if (body == null) { + return; + } + String username = body.getString("username"); + String topic = body.getString("topic"); + if (StrUtil.hasBlank(username, topic)) { + log.info("[handleAcl][ACL 参数不完整: username={}, topic={}]", username, topic); + sendAuthResponse(context, RESULT_IGNORE); + return; + } + // 1.2 解析设备身份 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + if (deviceInfo == null) { + sendAuthResponse(context, RESULT_IGNORE); + return; + } + // 1.3 解析 ACL 动作(兼容多种 EMQX 版本/插件字段) + Boolean subscribe = parseAclSubscribeFlag(body); + if (subscribe == null) { + sendAuthResponse(context, RESULT_IGNORE); + return; + } + + // 2. 执行 ACL 校验 + boolean allowed = subscribe + ? IotMqttTopicUtils.isTopicSubscribeAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName()) + : IotMqttTopicUtils.isTopicPublishAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + sendAuthResponse(context, allowed ? RESULT_ALLOW : RESULT_DENY); + } catch (Exception e) { + log.error("[handleAcl][ACL 处理失败][body={}]", body, e); + sendAuthResponse(context, RESULT_IGNORE); + } + } + + /** + * 解析 ACL 动作类型:订阅/发布 + * + * @param body ACL 请求体 + * @return true 订阅;false 发布;null 不识别 + */ + private static Boolean parseAclSubscribeFlag(JsonObject body) { + // 1. action 字段(常见为 publish/subscribe) + String action = body.getString("action"); + if (StrUtil.isNotBlank(action)) { + String lower = action.toLowerCase(Locale.ROOT); + if (lower.contains("sub")) { + return true; + } + if (lower.contains("pub")) { + return false; + } + } + + // 2. access 字段:可能是数字或字符串 + Integer access = body.getInteger("access"); + if (access != null) { + if (access == 1) { + return true; + } + if (access == 2) { + return false; + } + } + String accessText = body.getString("access"); + if (StrUtil.isNotBlank(accessText)) { + String lower = accessText.toLowerCase(Locale.ROOT); + if (lower.contains("sub")) { + return true; + } + if (lower.contains("pub")) { + return false; + } + if (StrUtil.isNumeric(accessText)) { + int value = Integer.parseInt(accessText); + if (value == 1) { + return true; + } + if (value == 2) { + return false; + } + } + } + return null; + } + + // ========== 事件处理 ========== + /** * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件 * 支持的事件类型:client.connected、client.disconnected 等 @@ -124,58 +301,15 @@ public class IotEmqxAuthEventHandler { break; } - // EMQX Webhook 只需要 200 状态码,无需响应体 + // 3. EMQX Webhook 只需要 200 状态码,无需响应体 context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); } catch (Exception e) { - log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e); - // 即使处理失败,也返回 200 避免EMQX重试 + log.error("[handleEvent][事件处理失败][body={}]", body, e); + // 即使处理失败,也返回 200 避免 EMQX 重试 context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); } } - /** - * 处理客户端连接事件 - */ - private void handleClientConnected(JsonObject body) { - String username = body.getString("username"); - log.info("[handleClientConnected][设备上线: {}]", username); - handleDeviceStateChange(username, true); - } - - /** - * 处理客户端断开连接事件 - */ - private void handleClientDisconnected(JsonObject body) { - String username = body.getString("username"); - String reason = body.getString("reason"); - log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason); - handleDeviceStateChange(username, false); - } - - /** - * 解析认证接口请求体 - *

                              - * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段) - * - * @param context 路由上下文 - * @return 请求体JSON对象,解析失败时返回null - */ - private JsonObject parseRequestBody(RoutingContext context) { - try { - JsonObject body = context.body().asJsonObject(); - if (body == null) { - log.info("[parseRequestBody][请求体为空]"); - sendAuthResponse(context, RESULT_IGNORE); - return null; - } - return body; - } catch (Exception e) { - log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e); - sendAuthResponse(context, RESULT_IGNORE); - return null; - } - } - /** * 解析事件接口请求体 *

                              @@ -201,23 +335,22 @@ public class IotEmqxAuthEventHandler { } /** - * 执行设备认证 - * - * @param clientId 客户端ID - * @param username 用户名 - * @param password 密码 - * @return 认证是否成功 + * 处理客户端连接事件 */ - private boolean handleDeviceAuth(String clientId, String username, String password) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(clientId).setUsername(username).setPassword(password)); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e); - throw e; - } + private void handleClientConnected(JsonObject body) { + String username = body.getString("username"); + log.info("[handleClientConnected][设备上线: {}]", username); + handleDeviceStateChange(username, true); + } + + /** + * 处理客户端断开连接事件 + */ + private void handleClientDisconnected(JsonObject body) { + String username = body.getString("username"); + String reason = body.getString("reason"); + log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason); + handleDeviceStateChange(username, false); } /** @@ -247,29 +380,4 @@ public class IotEmqxAuthEventHandler { } } - /** - * 发送 EMQX 认证响应 - * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应 - * - * @param context 路由上下文 - * @param result 认证结果:allow、deny、ignore - */ - private void sendAuthResponse(RoutingContext context, String result) { - // 构建符合 EMQX 官方规范的响应 - JsonObject response = new JsonObject() - .put("result", result) - .put("is_superuser", false); - - // 可以根据业务需求添加客户端属性 - // response.put("client_attrs", new JsonObject().put("role", "device")); - - // 可以添加认证过期时间(可选) - // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); - - context.response() - .setStatusCode(SUCCESS_STATUS_CODE) - .putHeader("Content-Type", "application/json; charset=utf-8") - .end(response.encode()); - } - -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java index 5ff8d120dc..4c2fa488f9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.mqtt.messages.MqttPublishMessage; import lombok.extern.slf4j.Slf4j; @@ -20,16 +19,16 @@ public class IotEmqxUpstreamHandler { private final String serverId; - public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) { + public IotEmqxUpstreamHandler(String serverId) { this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); - this.serverId = protocol.getServerId(); + this.serverId = serverId; } /** * 处理 MQTT 发布消息 */ public void handle(MqttPublishMessage mqttMessage) { - log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload()); + log.debug("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload()); String topic = mqttMessage.topicName(); byte[] payload = mqttMessage.payload().getBytes(); try { @@ -57,4 +56,4 @@ public class IotEmqxUpstreamHandler { } } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java index 2a842966fe..249b31544f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java @@ -38,6 +38,12 @@ public final class IotMqttTopicUtils { */ public static final String MQTT_EVENT_PATH = "/mqtt/event"; + /** + * MQTT ACL 接口路径 + * 对应 EMQX HTTP ACL 插件的 ACL 请求接口 + */ + public static final String MQTT_ACL_PATH = "/mqtt/acl"; + // ========== 工具方法 ========== /** @@ -85,4 +91,28 @@ public final class IotMqttTopicUtils { || topic.equals(SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/#"); } -} \ No newline at end of file + /** + * 校验主题是否允许发布 + *

                              + * 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头,且不允许包含通配符(+/#)。 + * + * @param topic 发布的主题 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @return 是否允许发布 + */ + // TODO DONE @AI:这个逻辑,是不是 mqtt 协议,也要使用???答:是通用工具方法,MQTT 协议可按需调用; + // TODO @AI:那你改下 mqtt,也调用!!! + public static boolean isTopicPublishAllowed(String topic, String productKey, String deviceName) { + if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { + return false; + } + // MQTT publish topic 不允许包含通配符,但这里做一次兜底校验 + if (topic.contains("#") || topic.contains("+")) { + return false; + } + String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/"; + return topic.startsWith(deviceTopicPrefix); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 4916c0d238..aaf19dc1c7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -125,43 +125,49 @@ yudao: max-message-size: 8192 # 最大消息大小(字节) connect-timeout-seconds: 60 # 连接超时时间(秒) ssl-enabled: false # 是否启用 SSL - - # 协议配置(旧版,保持兼容) - protocol: # ==================================== # 针对引入的 EMQX 组件的配置 # ==================================== - emqx: + - id: emqx-1 enabled: true - http-port: 8090 # MQTT HTTP 服务端口 - mqtt-host: 127.0.0.1 # MQTT Broker 地址 - mqtt-port: 1883 # MQTT Broker 端口 - mqtt-username: admin # MQTT 用户名 - mqtt-password: public # MQTT 密码 - mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID - mqtt-ssl: false # 是否开启 SSL - mqtt-topics: - - "/sys/#" # 系统主题 - clean-session: true # 是否启用 Clean Session (默认: true) - keep-alive-interval-seconds: 60 # 心跳间隔,单位秒 (默认: 60) - max-inflight-queue: 10000 # 最大飞行消息队列,单位:条 - connect-timeout-seconds: 10 # 连接超时,单位:秒 - # 是否信任所有 SSL 证书 (默认: false)。警告:生产环境必须为 false! - # 仅在开发环境或内网测试时,如果使用了自签名证书,可以临时设置为 true - trust-all: true # 在 dev 环境可以设为 true - # 遗嘱消息配置 (用于网关异常下线时通知其他系统) - will: - enabled: true # 生产环境强烈建议开启 - topic: "gateway/status/${yudao.iot.gateway.emqx.mqtt-client-id}" # 遗嘱消息主题 - payload: "offline" # 遗嘱消息负载 - qos: 1 # 遗嘱消息 QoS - retain: true # 遗嘱消息是否保留 - # 高级 SSL/TLS 配置 (当 trust-all: false 且 mqtt-ssl: true 时生效) - ssl-options: - key-store-path: "classpath:certs/client.jks" # 客户端证书库路径 - key-store-password: "your-keystore-password" # 客户端证书库密码 - trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 - trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 + protocol: emqx + port: 8090 # EMQX HTTP Hook 端口(/mqtt/auth、/mqtt/event) + emqx: + mqtt-host: 127.0.0.1 # MQTT Broker 地址 + mqtt-port: 1883 # MQTT Broker 端口 + mqtt-username: admin # MQTT 用户名 + mqtt-password: public # MQTT 密码 + mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID + mqtt-ssl: false # 是否开启 SSL + mqtt-topics: + - "/sys/#" # 系统主题 + mqtt-qos: 1 # 默认 QoS + clean-session: true # 是否启用 Clean Session (默认: true) + keep-alive-interval-seconds: 60 # 心跳间隔,单位秒 (默认: 60) + max-inflight-queue: 10000 # 最大飞行消息队列,单位:条 + connect-timeout-seconds: 10 # 连接超时,单位:秒 + reconnect-delay-ms: 5000 # 重连延迟,单位:毫秒 + # 是否信任所有 SSL 证书 (默认: false)。警告:生产环境必须为 false! + # 仅在开发环境或内网测试时,如果使用了自签名证书,可以临时设置为 true + trust-all: true # 在 dev 环境可以设为 true + # EMQX HTTP Hook 回调网关的 HTTPS 配置(可选) + http: + ssl-enabled: false + # ssl-cert-path: "path/to/server.crt" + # ssl-key-path: "path/to/server.key" + # 遗嘱消息配置 (用于网关异常下线时通知其他系统) + will: + enabled: true # 生产环境强烈建议开启 + topic: "gateway/status/iot-gateway-mqtt" # 遗嘱消息主题 + payload: "offline" # 遗嘱消息负载 + qos: 1 # 遗嘱消息 QoS + retain: true # 遗嘱消息是否保留 + # 高级 SSL/TLS 配置 (当 trust-all: false 且 mqtt-ssl: true 时生效) + ssl-options: + key-store-path: "classpath:certs/client.jks" # 客户端证书库路径 + key-store-password: "your-keystore-password" # 客户端证书库密码 + trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 + trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 --- #################### 日志相关配置 #################### @@ -181,7 +187,6 @@ logging: cn.iocoder.yudao.module.iot.gateway.protocol.emqx: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG - cn.iocoder.yudao.module.iot.gateway.protocol.mqttws: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG # 根日志级别 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotDirectDeviceEmqxProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotDirectDeviceEmqxProtocolIntegrationTest.java deleted file mode 100644 index a2e85919a5..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotDirectDeviceEmqxProtocolIntegrationTest.java +++ /dev/null @@ -1,437 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.http.HttpResponse; -import cn.hutool.http.HttpUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.mqtt.MqttClient; -import io.vertx.mqtt.MqttClientOptions; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.util.concurrent.TimeUnit; - -/** - * IoT 直连设备 EMQX 协议集成测试(手动测试) - * - *

                              测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 EMQX Broker 连接平台 - * - *

                              EMQX 协议架构: - *

                              - *     +--------+       MQTT        +-------------+       HTTP Hook        +---------+
                              - *     | 设备   | ----------------> | EMQX Broker | --------------------> | 网关    |
                              - *     +--------+                   +-------------+                        +---------+
                              - *         |                              |                                     |
                              - *         | 1. 连接认证                   | 2. 调用 /mqtt/auth                   |
                              - *         | 3. 发布消息                   | 4. 调用 /mqtt/event (上线/下线)       |
                              - *         |                              | 5. 网关订阅 EMQX 消息                 |
                              - *         |                              |                                     |
                              - * 
                              - * - *

                              测试分类: - *

                                - *
                              • 第一部分:模拟设备连接 EMQX Broker,发送 MQTT 消息
                              • - *
                              • 第二部分:模拟 EMQX Server 调用网关 HTTP Hook 接口(认证、事件)
                              • - *
                              - * - *

                              使用步骤: - *

                                - *
                              1. 启动 EMQX Broker(MQTT 端口 1883)
                              2. - *
                              3. 启动 yudao-module-iot-gateway 服务(HTTP 端口 18083)
                              4. - *
                              5. 配置 EMQX HTTP 认证插件指向网关的 /mqtt/auth 接口
                              6. - *
                              7. 配置 EMQX Webhook 插件指向网关的 /mqtt/event 接口
                              8. - *
                              9. 运行测试方法
                              10. - *
                              - * - * @author 芋道源码 - */ -@Slf4j -@Disabled -@SuppressWarnings("HttpUrlsUsage") -public class IotDirectDeviceEmqxProtocolIntegrationTest { - - private static final String SERVER_HOST = "127.0.0.1"; - /** - * EMQX 认证事件 HTTP 接口端口(网关提供给 EMQX Server 调用) - */ - private static final int HTTP_PORT = 18083; - /** - * EMQX Broker MQTT 端口(设备连接 EMQX) - */ - private static final int MQTT_PORT = 1883; - private static final int TIMEOUT_SECONDS = 10; - - private static Vertx vertx; - - // ===================== 编解码器(EMQX 使用 Alink 协议) ===================== - - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); - - // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== - - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; - private static final String DEVICE_NAME = "small"; - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; - - @BeforeAll - public static void setUp() { - vertx = Vertx.vertx(); - } - - @AfterAll - public static void tearDown() { - if (vertx != null) { - vertx.close(); - } - } - - // ================================================================================== - // 第一部分:模拟设备连接 EMQX Broker - // ================================================================================== - - /** - * 设备连接测试:模拟设备连接 EMQX Broker - *

                              - * 当设备连接 EMQX 时,EMQX 会自动调用网关的 /mqtt/auth 接口进行认证 - */ - @Test - public void testDeviceConnect() throws Exception { - // 1. 构建认证信息 - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - log.info("[testDeviceConnect][认证信息: clientId={}, username={}, password={}]", - authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); - - // 2. 创建客户端并连接 EMQX Broker - MqttClient client = createClient(authInfo); - try { - client.connect(MQTT_PORT, SERVER_HOST) - .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - log.info("[testDeviceConnect][连接 EMQX Broker 成功,客户端 ID: {}]", client.clientId()); - log.info("[testDeviceConnect][EMQX 会自动调用网关的 /mqtt/auth 接口进行认证]"); - log.info("[testDeviceConnect][EMQX 会自动调用网关的 /mqtt/event 接口通知设备上线]"); - } finally { - disconnect(client); - log.info("[testDeviceConnect][EMQX 会自动调用网关的 /mqtt/event 接口通知设备下线]"); - } - } - - /** - * 属性上报测试:设备通过 EMQX Broker 发布属性消息 - *

                              - * 消息流程:设备 -> EMQX Broker -> 网关(订阅 EMQX 消息) - */ - @Test - public void testPropertyPost() throws Exception { - // 1. 连接 EMQX Broker - MqttClient client = connectToEmqx(); - log.info("[testPropertyPost][连接 EMQX Broker 成功]"); - - try { - // 2.1 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.requestOf( - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("width", 1) - .put("height", "2") - .build())); - - // 2.2 发布消息到 EMQX Broker - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); - publish(client, topic, request); - log.info("[testPropertyPost][属性上报消息已发送到 EMQX Broker]"); - log.info("[testPropertyPost][网关会通过订阅 EMQX 接收此消息]"); - - // 2.3 等待消息处理 - Thread.sleep(2000); - log.info("[testPropertyPost][请检查网关日志确认消息是否被正确处理]"); - } finally { - disconnect(client); - } - } - - /** - * 事件上报测试:设备通过 EMQX Broker 发布事件消息 - *

                              - * 消息流程:设备 -> EMQX Broker -> 网关(订阅 EMQX 消息) - */ - @Test - public void testEventPost() throws Exception { - // 1. 连接 EMQX Broker - MqttClient client = connectToEmqx(); - log.info("[testEventPost][连接 EMQX Broker 成功]"); - - try { - // 2.1 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.requestOf( - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "eat", - MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis())); - - // 2.2 发布消息到 EMQX Broker - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); - publish(client, topic, request); - log.info("[testEventPost][事件上报消息已发送到 EMQX Broker]"); - log.info("[testEventPost][网关会通过订阅 EMQX 接收此消息]"); - - // 2.3 等待消息处理 - Thread.sleep(2000); - log.info("[testEventPost][请检查网关日志确认消息是否被正确处理]"); - } finally { - disconnect(client); - } - } - - /** - * 订阅下行消息测试:设备订阅服务端下发的消息 - *

                              - * 消息流程:网关 -> EMQX Broker -> 设备 - */ - @Test - public void testSubscribe() throws Exception { - // 1. 连接 EMQX Broker - MqttClient client = connectToEmqx(); - log.info("[testSubscribe][连接 EMQX Broker 成功]"); - - try { - // 2. 设置消息处理器 - client.publishHandler(message -> log.info("[testSubscribe][收到下行消息: topic={}, payload={}]", - message.topicName(), message.payload().toString())); - - // 3. 订阅下行主题 - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); - log.info("[testSubscribe][订阅主题: {}]", topic); - subscribe(client, topic); - log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); - log.info("[testSubscribe][网关下发的消息会通过 EMQX Broker 转发给设备]"); - - // 4. 保持连接 30 秒等待消息 - Thread.sleep(30000); - } finally { - disconnect(client); - } - } - - // ================================================================================== - // 第二部分:模拟 EMQX Server 调用网关 HTTP Hook 接口 - // 说明:这些接口是 EMQX Server 自动调用的,这里只是用于单独测试接口功能 - // ================================================================================== - - /** - * 认证接口测试:模拟 EMQX Server 调用 /mqtt/auth 接口 - *

                              - * 注意:正常情况下此接口由 EMQX HTTP 认证插件自动调用,这里只是测试接口本身 - */ - @Test - public void testEmqxAuthHook() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/mqtt/auth", SERVER_HOST, HTTP_PORT); - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - // 1.2 EMQX 认证请求格式 - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("clientid", authInfo.getClientId()) - .put("username", authInfo.getUsername()) - .put("password", authInfo.getPassword()) - .build()); - // 1.3 输出请求 - log.info("[testEmqxAuthHook][模拟 EMQX Server 调用认证接口]"); - log.info("[testEmqxAuthHook][请求 URL: {}]", url); - log.info("[testEmqxAuthHook][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Content-Type", "application/json") - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testEmqxAuthHook][响应状态码: {}]", httpResponse.getStatus()); - log.info("[testEmqxAuthHook][响应体: {}]", httpResponse.body()); - log.info("[testEmqxAuthHook][认证结果: result=allow 表示认证成功, result=deny 表示认证失败]"); - } - } - - /** - * 认证失败测试:模拟 EMQX Server 调用 /mqtt/auth 接口(错误密码) - */ - @Test - public void testEmqxAuthHookFailed() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/mqtt/auth", SERVER_HOST, HTTP_PORT); - // 1.2 使用错误的密码 - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("clientid", PRODUCT_KEY + "." + DEVICE_NAME) - .put("username", DEVICE_NAME + "&" + PRODUCT_KEY) - .put("password", "wrong_password") - .build()); - // 1.3 输出请求 - log.info("[testEmqxAuthHookFailed][模拟 EMQX Server 调用认证接口(错误密码)]"); - log.info("[testEmqxAuthHookFailed][请求 URL: {}]", url); - log.info("[testEmqxAuthHookFailed][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Content-Type", "application/json") - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testEmqxAuthHookFailed][响应状态码: {}]", httpResponse.getStatus()); - log.info("[testEmqxAuthHookFailed][响应体: {}]", httpResponse.body()); - log.info("[testEmqxAuthHookFailed][预期结果: result=deny]"); - } - } - - /** - * 设备上线事件测试:模拟 EMQX Server Webhook 调用 /mqtt/event 接口 - *

                              - * 注意:正常情况下此接口由 EMQX Webhook 插件自动调用,这里只是测试接口本身 - */ - @Test - public void testEmqxClientConnectedHook() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/mqtt/event", SERVER_HOST, HTTP_PORT); - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - // 1.2 EMQX Webhook client.connected 事件格式 - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("event", "client.connected") - .put("clientid", authInfo.getClientId()) - .put("username", authInfo.getUsername()) - .put("peername", "127.0.0.1:12345") - .put("connected_at", System.currentTimeMillis()) - .build()); - // 1.3 输出请求 - log.info("[testEmqxClientConnectedHook][模拟 EMQX Server Webhook 调用设备上线事件]"); - log.info("[testEmqxClientConnectedHook][请求 URL: {}]", url); - log.info("[testEmqxClientConnectedHook][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Content-Type", "application/json") - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testEmqxClientConnectedHook][响应状态码: {}]", httpResponse.getStatus()); - log.info("[testEmqxClientConnectedHook][响应体: {}]", httpResponse.body()); - log.info("[testEmqxClientConnectedHook][预期结果: 状态码 200,设备状态更新为在线]"); - } - } - - /** - * 设备下线事件测试:模拟 EMQX Server Webhook 调用 /mqtt/event 接口 - *

                              - * 注意:正常情况下此接口由 EMQX Webhook 插件自动调用,这里只是测试接口本身 - */ - @Test - public void testEmqxClientDisconnectedHook() { - // 1.1 构建请求 - String url = String.format("http://%s:%d/mqtt/event", SERVER_HOST, HTTP_PORT); - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - // 1.2 EMQX Webhook client.disconnected 事件格式 - String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("event", "client.disconnected") - .put("clientid", authInfo.getClientId()) - .put("username", authInfo.getUsername()) - .put("reason", "normal") - .put("disconnected_at", System.currentTimeMillis()) - .build()); - // 1.3 输出请求 - log.info("[testEmqxClientDisconnectedHook][模拟 EMQX Server Webhook 调用设备下线事件]"); - log.info("[testEmqxClientDisconnectedHook][请求 URL: {}]", url); - log.info("[testEmqxClientDisconnectedHook][请求体: {}]", payload); - - // 2.1 发送请求 - try (HttpResponse httpResponse = HttpUtil.createPost(url) - .header("Content-Type", "application/json") - .body(payload) - .execute()) { - // 2.2 输出结果 - log.info("[testEmqxClientDisconnectedHook][响应状态码: {}]", httpResponse.getStatus()); - log.info("[testEmqxClientDisconnectedHook][响应体: {}]", httpResponse.body()); - log.info("[testEmqxClientDisconnectedHook][预期结果: 状态码 200,设备状态更新为离线]"); - } - } - - // ===================== 辅助方法 ===================== - - /** - * 创建 MQTT 客户端 - * - * @param authInfo 认证信息 - * @return MQTT 客户端 - */ - private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { - MqttClientOptions options = new MqttClientOptions() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()) - .setCleanSession(true) - .setKeepAliveInterval(60); - return MqttClient.create(vertx, options); - } - - /** - * 连接 EMQX Broker 并认证设备 - * - * @return 已认证的 MQTT 客户端 - */ - private MqttClient connectToEmqx() throws Exception { - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - MqttClient client = createClient(authInfo); - client.connect(MQTT_PORT, SERVER_HOST) - .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - return client; - } - - /** - * 订阅主题 - * - * @param client MQTT 客户端 - * @param topic 主题 - */ - private void subscribe(MqttClient client, String topic) throws Exception { - client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) - .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - log.info("[subscribe][订阅主题成功: {}]", topic); - } - - /** - * 发布消息 - * - * @param client MQTT 客户端 - * @param topic 发布主题 - * @param request 请求消息 - */ - private void publish(MqttClient client, String topic, IotDeviceMessage request) throws Exception { - byte[] payload = CODEC.encode(request); - log.info("[publish][发送消息: topic={}, payload={}]", topic, new String(payload)); - client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - log.info("[publish][消息发布成功]"); - } - - /** - * 断开连接 - * - * @param client MQTT 客户端 - */ - private void disconnect(MqttClient client) throws Exception { - client.disconnect() - .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - log.info("[disconnect][断开连接成功]"); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java new file mode 100644 index 0000000000..d7d4535458 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java @@ -0,0 +1,18 @@ +/** + * IoT 网关 EMQX 协议集成测试包 + * + *

                              + * 测试类直接使用 mqtt 包下的单测即可,因为设备都是通过 MQTT 协议连接 EMQX Broker。 + * + * @see cn.iocoder.yudao.module.iot.gateway.protocol.mqtt + * + *

                              架构

                              + *
                              + * +--------+      MQTT       +-------------+     HTTP Hook     +---------+
                              + * |  设备  | --------------> | EMQX Broker | ----------------> |  网关   |
                              + * +--------+                 +-------------+                   +---------+
                              + * 
                              + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; From 85c1b05bcaa51cad318ee2ca35ddf063dfd6bea0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 3 Feb 2026 20:21:19 +0800 Subject: [PATCH 27/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91emqx=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=8850%=EF=BC=89?= =?UTF-8?q?=EF=BC=9A=E4=BF=AE=E5=A4=8D=20Review=20Agent=20=E5=8F=8D?= =?UTF-8?q?=E9=A6=88=E7=9A=84=20bug=EF=BC=88=E5=A2=9E=E5=BC=BA=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=E6=80=A7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../protocol/emqx/IotEmqxProtocol.java | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java index 226e421024..5072159256 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java @@ -176,7 +176,8 @@ public class IotEmqxProtocol implements IotProtocol { // 2.4 关闭 Vertx if (vertx != null) { try { - vertx.close().result(); + vertx.close().toCompletionStage().toCompletableFuture() + .get(10, TimeUnit.SECONDS); log.info("[stop][IoT EMQX 协议 {} Vertx 已关闭]", getId()); } catch (Exception e) { log.error("[stop][IoT EMQX 协议 {} Vertx 关闭失败]", getId(), e); @@ -195,7 +196,7 @@ public class IotEmqxProtocol implements IotProtocol { private void startHttpServer() { // 1. 创建路由 Router router = Router.router(vertx); - router.route().handler(BodyHandler.create()); + router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB,防止大包攻击 // 2. 创建处理器 IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId); @@ -218,12 +219,13 @@ public class IotEmqxProtocol implements IotProtocol { httpServer = vertx.createHttpServer(options) .requestHandler(router) .listen() - .result(); + .toCompletionStage().toCompletableFuture() + .get(10, TimeUnit.SECONDS); log.info("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动成功, port: {}, ssl: {}]", getId(), properties.getPort(), httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())); } catch (Exception e) { log.error("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动失败, port: {}]", getId(), properties.getPort(), e); - throw e; + throw new RuntimeException("HTTP Hook 服务启动失败", e); } } @@ -232,7 +234,8 @@ public class IotEmqxProtocol implements IotProtocol { return; } try { - httpServer.close().result(); + httpServer.close().toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); log.info("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务已停止]", getId()); } catch (Exception e) { log.error("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务停止失败]", getId(), e); @@ -258,19 +261,21 @@ public class IotEmqxProtocol implements IotProtocol { private void stopMqttClient() { MqttClient client = this.mqttClient; - if (client == null || !client.isConnected()) { + this.mqttClient = null; // 先清理引用 + if (client == null) { return; } - this.mqttClient = null; - // 1. 批量取消订阅 - List topicList = emqxConfig.getMqttTopics(); - if (CollUtil.isNotEmpty(topicList)) { - try { - client.unsubscribe(topicList).toCompletionStage().toCompletableFuture() - .get(5, TimeUnit.SECONDS); - } catch (Exception e) { - log.warn("[stopMqttClient][IoT EMQX 协议 {} 取消订阅异常]", getId(), e); + // 1. 批量取消订阅(仅在连接时) + if (client.isConnected()) { + List topicList = emqxConfig.getMqttTopics(); + if (CollUtil.isNotEmpty(topicList)) { + try { + client.unsubscribe(topicList).toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); + } catch (Exception e) { + log.warn("[stopMqttClient][IoT EMQX 协议 {} 取消订阅异常]", getId(), e); + } } } @@ -296,15 +301,15 @@ public class IotEmqxProtocol implements IotProtocol { .setClientId(emqxConfig.getMqttClientId()) .setUsername(emqxConfig.getMqttUsername()) .setPassword(emqxConfig.getMqttPassword()) - .setSsl(emqxConfig.getMqttSsl()) - .setCleanSession(emqxConfig.getCleanSession()) + .setSsl(Boolean.TRUE.equals(emqxConfig.getMqttSsl())) + .setCleanSession(Boolean.TRUE.equals(emqxConfig.getCleanSession())) .setKeepAliveInterval(emqxConfig.getKeepAliveIntervalSeconds()) .setMaxInflightQueue(emqxConfig.getMaxInflightQueue()); options.setConnectTimeout(emqxConfig.getConnectTimeoutSeconds() * 1000); // Vert.x 需要毫秒 - options.setTrustAll(emqxConfig.getTrustAll()); + options.setTrustAll(Boolean.TRUE.equals(emqxConfig.getTrustAll())); // 1.2 配置遗嘱消息 IotEmqxConfig.Will will = emqxConfig.getWill(); - if (will.isEnabled()) { + if (will != null && will.isEnabled()) { Assert.notBlank(will.getTopic(), "遗嘱消息主题(emqx.will.topic)不能为空"); Assert.notNull(will.getPayload(), "遗嘱消息内容(emqx.will.payload)不能为空"); options.setWillFlag(true) @@ -313,9 +318,11 @@ public class IotEmqxProtocol implements IotProtocol { .setWillQoS(will.getQos()) .setWillRetain(will.isRetain()); } - // 1.3 配置高级 SSL/TLS(仅在启用 SSL 且不信任所有证书时生效) - if (Boolean.TRUE.equals(emqxConfig.getMqttSsl()) && !Boolean.TRUE.equals(emqxConfig.getTrustAll())) { - IotEmqxConfig.Ssl sslOptions = emqxConfig.getSslOptions(); + // 1.3 配置高级 SSL/TLS(仅在启用 SSL 且不信任所有证书时生效,且需要 sslOptions 非空) + IotEmqxConfig.Ssl sslOptions = emqxConfig.getSslOptions(); + if (Boolean.TRUE.equals(emqxConfig.getMqttSsl()) + && Boolean.FALSE.equals(emqxConfig.getTrustAll()) + && sslOptions != null) { if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) { options.setTrustStoreOptions(new JksOptions() .setPath(sslOptions.getTrustStorePath()) @@ -365,10 +372,11 @@ public class IotEmqxProtocol implements IotProtocol { */ private void closeMqttClient() { MqttClient oldClient = this.mqttClient; - if (oldClient == null || !oldClient.isConnected()) { + this.mqttClient = null; // 先清理引用 + if (oldClient == null) { return; } - this.mqttClient = null; + // 尽力释放(无论是否连接都尝试 disconnect) try { oldClient.disconnect().toCompletionStage().toCompletableFuture() .get(5, TimeUnit.SECONDS); @@ -391,7 +399,11 @@ public class IotEmqxProtocol implements IotProtocol { return; } log.info("[startMqttClientReconnectChecker][IoT EMQX 协议 {} 检测到断开,尝试重连]", getId()); - tryReconnectMqttClient(); + // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待) + vertx.executeBlocking(() -> { + tryReconnectMqttClient(); + return null; + }); }); } @@ -449,7 +461,11 @@ public class IotEmqxProtocol implements IotProtocol { return; } log.warn("[setupMqttClientHandlers][IoT EMQX 协议 {} 连接断开,立即尝试重连]", getId()); - vertx.runOnContext(v -> tryReconnectMqttClient()); + // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待) + vertx.executeBlocking(() -> { + tryReconnectMqttClient(); + return null; + }); }); // 2. 异常处理 @@ -497,7 +513,8 @@ public class IotEmqxProtocol implements IotProtocol { return; } MqttQoS qos = MqttQoS.valueOf(emqxConfig.getMqttQos()); - mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false); + mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false) + .onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e)); } } From cc0d786d0fd62e075da186d45c4e504a22f9a769 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 3 Feb 2026 22:29:06 +0800 Subject: [PATCH 28/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91emqx=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=B9=E9=80=A0=EF=BC=8860%=EF=BC=89?= =?UTF-8?q?=EF=BC=9A=E6=94=AF=E6=8C=81=20device=20register=20=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../protocol/emqx/IotEmqxProtocol.java | 13 ++- .../upstream/IotEmqxAuthEventHandler.java | 93 ++++++++++++++++++- .../src/main/resources/application.yaml | 6 +- ...rectDeviceMqttProtocolIntegrationTest.java | 15 +-- 4 files changed, 115 insertions(+), 12 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java index 5072159256..0d64186aa5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java @@ -199,7 +199,7 @@ public class IotEmqxProtocol implements IotProtocol { router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB,防止大包攻击 // 2. 创建处理器 - IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId); + IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId, this); router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl); router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); @@ -517,4 +517,15 @@ public class IotEmqxProtocol implements IotProtocol { .onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e)); } + /** + * 延迟发布消息到 MQTT Broker + * + * @param topic 主题 + * @param payload 消息内容 + * @param delayMs 延迟时间(毫秒) + */ + public void publishDelayMessage(String topic, byte[] payload, long delayMs) { + vertx.setTimer(delayMs, id -> publishMessage(topic, payload)); + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java index 0ba250cb1a..53705aa64e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java @@ -1,14 +1,19 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; import io.vertx.core.json.JsonObject; @@ -24,6 +29,7 @@ import java.util.Locale; * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)} * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)} * 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)} + * 4. 设备注册接口 - 集成一型一密设备注册 {@link #handleDeviceRegister(RoutingContext, String, String)} * * @author 芋道源码 */ @@ -57,13 +63,21 @@ public class IotEmqxAuthEventHandler { */ private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected"; + /** + * 认证类型标识 - 设备注册 + */ + private static final String AUTH_TYPE_REGISTER = "|authType=register|"; + private final String serverId; + private final IotEmqxProtocol protocol; + private final IotDeviceMessageService deviceMessageService; private final IotDeviceCommonApi deviceApi; - public IotEmqxAuthEventHandler(String serverId) { + public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) { this.serverId = serverId; + this.protocol = protocol; this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); } @@ -91,7 +105,13 @@ public class IotEmqxAuthEventHandler { return; } - // 2. 执行认证 + // 2.1 情况一:判断是否为注册请求 + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) { + handleDeviceRegister(context, username, password); + return; + } + + // 2.2 情况二:执行认证 boolean authResult = handleDeviceAuth(clientId, username, password); log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult); if (authResult) { @@ -380,4 +400,73 @@ public class IotEmqxAuthEventHandler { } } + // ========= 注册处理 ========= + + /** + * 处理设备注册请求(一型一密) + * + * @param context 路由上下文 + * @param username 用户名 + * @param password 密码(签名) + */ + private void handleDeviceRegister(RoutingContext context, String username, String password) { + try { + // 1. 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + if (deviceInfo == null) { + log.warn("[handleDeviceRegister][设备注册失败: 无法解析 username={}]", username); + sendAuthResponse(context, RESULT_DENY); + return; + } + + // 2. 调用注册 API + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO() + .setProductKey(deviceInfo.getProductKey()) + .setDeviceName(deviceInfo.getDeviceName()) + .setSign(password); + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); + + // 3. 允许连接 + log.info("[handleDeviceRegister][设备注册成功: {}]", username); + sendAuthResponse(context, RESULT_ALLOW); + + // 4. 延迟 5 秒发送注册结果(等待设备连接成功并完成订阅) + sendRegisterResultMessage(username, result.getData()); + } catch (Exception e) { + log.warn("[handleDeviceRegister][设备注册失败: {}, 错误: {}]", username, e.getMessage()); + sendAuthResponse(context, RESULT_DENY); + } + } + + /** + * 发送注册结果消息给设备 + *

                              + * 注意:延迟 5 秒发送,等待设备连接成功并完成订阅。 + * + * @param username 用户名 + * @param result 注册结果 + */ + @SuppressWarnings("DataFlowIssue") + private void sendRegisterResultMessage(String username, IotDeviceRegisterRespDTO result) { + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + Assert.notNull(deviceInfo, "设备信息不能为空"); + try { + // 1. 构建响应消息 + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(null, method, result, 0, null); + + // 2. 编码消息 + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, "Alink"); + + // 3. 构建响应主题并延迟发布(等待设备连接成功并完成订阅) + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), true); + protocol.publishDelayMessage(replyTopic, encodedData, 5000); + log.info("[sendRegisterResultMessage][发送注册结果: topic={}]", replyTopic); + } catch (Exception e) { + log.error("[sendRegisterResultMessage][发送注册结果失败: {}]", username, e); + } + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index aaf19dc1c7..9c6b500d5b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -117,9 +117,9 @@ yudao: # 针对引入的 MQTT 组件的配置 # ==================================== - id: mqtt-json - enabled: false + enabled: true protocol: mqtt - port: 1884 + port: 1883 serialize: json mqtt: max-message-size: 8192 # 最大消息大小(字节) @@ -129,7 +129,7 @@ yudao: # 针对引入的 EMQX 组件的配置 # ==================================== - id: emqx-1 - enabled: true + enabled: false protocol: emqx port: 8090 # EMQX HTTP Hook 端口(/mqtt/auth、/mqtt/event) emqx: diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java index 415d15a4de..22ac321817 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java @@ -201,7 +201,12 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { MqttClient client = MqttClient.create(vertx, options); try { - // 2. 设置消息处理器,接收注册响应 + // 2. 连接服务器(连接成功后服务端会自动处理注册并发送响应) + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testDeviceRegister][连接成功,等待注册响应...]"); + + // 3.1 设置消息处理器,接收注册响应 CompletableFuture responseFuture = new CompletableFuture<>(); client.publishHandler(message -> { log.info("[testDeviceRegister][收到响应: topic={}, payload={}]", @@ -209,11 +214,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); responseFuture.complete(response); }); - - // 3. 连接服务器(连接成功后服务端会自动处理注册并发送响应) - client.connect(SERVER_PORT, SERVER_HOST) - .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - log.info("[testDeviceRegister][连接成功,等待注册响应...]"); + // 3.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", PRODUCT_KEY, deviceName); + subscribe(client, replyTopic); // 4. 等待注册响应 IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); From a77e1780ccfa9c42d325fe5da7d8e8a1fde7dfcf Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 4 Feb 2026 10:00:15 +0800 Subject: [PATCH 29/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=20codecType=EF=BC=8C=E4=BD=BF=E7=94=A8=20protocolType?= =?UTF-8?q?=20=E6=9B=BF=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/api/device/IoTDeviceApiImpl.java | 2 +- .../product/vo/product/IotProductRespVO.java | 13 +- .../vo/product/IotProductSaveReqVO.java | 14 +- .../dal/dataobject/product/IotProductDO.java | 14 +- .../module/iot/enums/DictTypeConstants.java | 4 +- .../iot/core/biz/dto/IotDeviceRespDTO.java | 8 +- .../gateway/codec/IotDeviceMessageCodec.java | 33 -- .../alink/IotAlinkDeviceMessageCodec.java | 89 ------ .../iot/gateway/codec/package-info.java | 4 - .../tcp/IotTcpBinaryDeviceMessageCodec.java | 286 ------------------ .../tcp/IotTcpJsonDeviceMessageCodec.java | 110 ------- .../downstream/IotEmqxDownstreamHandler.java | 5 +- .../upstream/IotEmqxAuthEventHandler.java | 13 +- .../upstream/IotEmqxUpstreamHandler.java | 14 +- .../downstream/IotMqttDownstreamHandler.java | 2 +- .../upstream/IotMqttAbstractHandler.java | 18 +- .../upstream/IotMqttRegisterHandler.java | 5 +- .../upstream/IotMqttUpstreamHandler.java | 2 +- .../iot/gateway/protocol/package-info.java | 4 +- .../iot/gateway/serialize/package-info.java | 6 + .../message/IotDeviceMessageService.java | 39 +-- .../message/IotDeviceMessageServiceImpl.java | 80 +++-- ...rectDeviceMqttProtocolIntegrationTest.java | 20 +- ...ewayDeviceMqttProtocolIntegrationTest.java | 18 +- ...ySubDeviceMqttProtocolIntegrationTest.java | 18 +- 25 files changed, 159 insertions(+), 662 deletions(-) delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java index db0a862d0e..ee4680ea6d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java @@ -58,7 +58,7 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> { IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId()); if (product != null) { - deviceDTO.setCodecType(product.getCodecType()); + deviceDTO.setProtocolType(product.getProtocolType()).setSerializeType(product.getSerializeType()); } })); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java index ffc92a2132..302b072620 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java @@ -67,10 +67,15 @@ public class IotProductRespVO { @DictFormat(DictTypeConstants.NET_TYPE) private Integer netType; - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - @ExcelProperty(value = "数据格式", converter = DictConvert.class) - @DictFormat(DictTypeConstants.CODEC_TYPE) - private String codecType; + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt") + @ExcelProperty(value = "协议类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PROTOCOL_TYPE) + private String protocolType; + + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json") + @ExcelProperty(value = "序列化类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SERIALIZE_TYPE) + private String serializeType; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java index 08c636f7f2..fceede0eb0 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum; import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; @@ -44,9 +46,15 @@ public class IotProductSaveReqVO { @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}") private Integer netType; - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - @NotEmpty(message = "数据格式不能为空") - private String codecType; + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt") + @InEnum(value = IotProtocolTypeEnum.class, message = "协议类型必须是 {value}") + @NotEmpty(message = "协议类型不能为空") + private String protocolType; + + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json") + @InEnum(value = IotSerializeTypeEnum.class, message = "序列化类型必须是 {value}") + @NotEmpty(message = "序列化类型不能为空") + private String serializeType; @Schema(description = "是否开启动态注册", example = "false") @NotNull(message = "是否开启动态注册不能为空") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java index e296b35017..a1a77fc5e4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java @@ -78,12 +78,16 @@ public class IotProductDO extends TenantBaseDO { */ private Integer netType; /** - * 数据格式(编解码器类型) + * 协议类型 *

                              - * 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE} - * - * 目的:用于 gateway-server 解析消息格式 + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum} */ - private String codecType; + private String protocolType; + /** + * 序列化类型 + *

                              + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum} + */ + private String serializeType; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java index 4f07ddfc1c..cf6bec1181 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java @@ -8,8 +8,8 @@ package cn.iocoder.yudao.module.iot.enums; public class DictTypeConstants { public static final String NET_TYPE = "iot_net_type"; - public static final String LOCATION_TYPE = "iot_location_type"; - public static final String CODEC_TYPE = "iot_codec_type"; + public static final String PROTOCOL_TYPE = "iot_protocol_type"; + public static final String SERIALIZE_TYPE = "iot_serialize_type"; public static final String PRODUCT_STATUS = "iot_product_status"; public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type"; diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java index add1167801..8ad2c5bcd0 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java @@ -34,8 +34,12 @@ public class IotDeviceRespDTO { */ private Long productId; /** - * 编解码器类型 + * 协议类型 */ - private String codecType; + private String protocolType; + /** + * 序列化类型 + */ + private String serializeType; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java deleted file mode 100644 index 94dd309dd1..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; - -/** - * {@link cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage} 的编解码器 - * - * @author 芋道源码 - */ -public interface IotDeviceMessageCodec { - - /** - * 编码消息 - * - * @param message 消息 - * @return 编码后的消息内容 - */ - byte[] encode(IotDeviceMessage message); - - /** - * 解码消息 - * - * @param bytes 消息内容 - * @return 解码后的消息内容 - */ - IotDeviceMessage decode(byte[] bytes); - - /** - * @return 数据格式(编码器类型) - */ - String type(); - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java deleted file mode 100644 index 5a4e47fe18..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java +++ /dev/null @@ -1,89 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.alink; - -import cn.hutool.core.lang.Assert; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.stereotype.Component; - -/** - * 阿里云 Alink {@link IotDeviceMessage} 的编解码器 - * - * @author 芋道源码 - */ -@Component -public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec { - - public static final String TYPE = "Alink"; - - @Data - @NoArgsConstructor - @AllArgsConstructor - private static class AlinkMessage { - - public static final String VERSION_1 = "1.0"; - - /** - * 消息 ID,且每个消息 ID 在当前设备具有唯一性 - */ - private String id; - - /** - * 版本号 - */ - private String version; - - /** - * 请求方法 - */ - private String method; - - /** - * 请求参数 - */ - private Object params; - - /** - * 响应结果 - */ - private Object data; - /** - * 响应错误码 - */ - private Integer code; - /** - * 响应提示 - * - * 特殊:这里阿里云是 message,为了保持和项目的 {@link CommonResult#getMsg()} 一致。 - */ - private String msg; - - } - - @Override - public String type() { - return TYPE; - } - - @Override - public byte[] encode(IotDeviceMessage message) { - AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1, - message.getMethod(), message.getParams(), message.getData(), message.getCode(), message.getMsg()); - return JsonUtils.toJsonByte(alinkMessage); - } - - @Override - @SuppressWarnings("DataFlowIssue") - public IotDeviceMessage decode(byte[] bytes) { - AlinkMessage alinkMessage = JsonUtils.parseObject(bytes, AlinkMessage.class); - Assert.notNull(alinkMessage, "消息不能为空"); - Assert.equals(alinkMessage.getVersion(), AlinkMessage.VERSION_1, "消息版本号必须是 1.0"); - return IotDeviceMessage.of(alinkMessage.getId(), alinkMessage.getMethod(), alinkMessage.getParams(), - alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg()); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java deleted file mode 100644 index e1dae7707a..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 提供设备接入的各种数据(请求、响应)的编解码 - */ -package cn.iocoder.yudao.module.iot.gateway.codec; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java deleted file mode 100644 index 05098cccbf..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java +++ /dev/null @@ -1,286 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import io.vertx.core.buffer.Buffer; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.nio.charset.StandardCharsets; - -/** - * TCP/UDP 二进制格式 {@link IotDeviceMessage} 编解码器 - *

                              - * 二进制协议格式(所有数值使用大端序): - * - *

                              - * +--------+--------+--------+---------------------------+--------+--------+
                              - * | 魔术字 | 版本号 | 消息类型|         消息长度(4 字节)          |
                              - * +--------+--------+--------+---------------------------+--------+--------+
                              - * |           消息 ID 长度(2 字节)        |      消息 ID (变长字符串)         |
                              - * +--------+--------+--------+--------+--------+--------+--------+--------+
                              - * |           方法名长度(2 字节)        |      方法名(变长字符串)         |
                              - * +--------+--------+--------+--------+--------+--------+--------+--------+
                              - * |                        消息体数据(变长)                              |
                              - * +--------+--------+--------+--------+--------+--------+--------+--------+
                              - * 
                              - *

                              - * 消息体格式: - * - 请求消息:params 数据(JSON) - * - 响应消息:code (4字节) + msg 长度(2字节) + msg 字符串 + data 数据(JSON) - *

                              - * 注意:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 - * - * @author 芋道源码 - */ -@Slf4j -@Component -public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { - - public static final String TYPE = "TCP_BINARY"; - - /** - * 协议魔术字,用于协议识别 - */ - private static final byte MAGIC_NUMBER = (byte) 0x7E; - - /** - * 协议版本号 - */ - private static final byte PROTOCOL_VERSION = (byte) 0x01; - - /** - * 请求消息类型 - */ - private static final byte REQUEST = (byte) 0x01; - - /** - * 响应消息类型 - */ - private static final byte RESPONSE = (byte) 0x02; - - /** - * 协议头部固定长度(魔术字 + 版本号 + 消息类型 + 消息长度) - */ - private static final int HEADER_FIXED_LENGTH = 7; - - /** - * 最小消息长度(头部 + 消息ID长度 + 方法名长度) - */ - private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4; - - @Override - public String type() { - return TYPE; - } - - @Override - public byte[] encode(IotDeviceMessage message) { - Assert.notNull(message, "消息不能为空"); - Assert.notBlank(message.getMethod(), "消息方法不能为空"); - try { - // 1. 确定消息类型 - byte messageType = determineMessageType(message); - // 2. 构建消息体 - byte[] bodyData = buildMessageBody(message, messageType); - // 3. 构建完整消息 - return buildCompleteMessage(message, messageType, bodyData); - } catch (Exception e) { - log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e); - throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e); - } - } - - @Override - public IotDeviceMessage decode(byte[] bytes) { - Assert.notNull(bytes, "待解码数据不能为空"); - Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足"); - try { - Buffer buffer = Buffer.buffer(bytes); - // 解析协议头部和消息内容 - int index = 0; - // 1. 验证魔术字 - byte magic = buffer.getByte(index++); - Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic); - - // 2. 验证版本号 - byte version = buffer.getByte(index++); - Assert.isTrue(version == PROTOCOL_VERSION, "不支持的协议版本: " + version); - - // 3. 读取消息类型 - byte messageType = buffer.getByte(index++); - // 直接验证消息类型,无需抽取方法 - Assert.isTrue(messageType == REQUEST || messageType == RESPONSE, - "无效的消息类型: " + messageType); - - // 4. 读取消息长度 - int messageLength = buffer.getInt(index); - index += 4; - Assert.isTrue(messageLength == buffer.length(), - "消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length()); - - // 5. 读取消息 ID - short messageIdLength = buffer.getShort(index); - index += 2; - String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name()); - index += messageIdLength; - - // 6. 读取方法名 - short methodLength = buffer.getShort(index); - index += 2; - String method = buffer.getString(index, index + methodLength, StandardCharsets.UTF_8.name()); - index += methodLength; - - // 7. 解析消息体 - return parseMessageBody(buffer, index, messageType, messageId, method); - } catch (Exception e) { - log.error("[decode][TCP 二进制消息解码失败,数据长度: {}]", bytes.length, e); - throw new RuntimeException("TCP 二进制消息解码失败: " + e.getMessage(), e); - } - } - - /** - * 确定消息类型 - * 优化后的判断逻辑:有响应字段就是响应消息,否则就是请求消息 - */ - private byte determineMessageType(IotDeviceMessage message) { - // 判断是否为响应消息:有响应码或响应消息时为响应 - if (message.getCode() != null) { - return RESPONSE; - } - // 默认为请求消息 - return REQUEST; - } - - /** - * 构建消息体 - */ - private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) { - Buffer bodyBuffer = Buffer.buffer(); - if (messageType == RESPONSE) { - // code - bodyBuffer.appendInt(message.getCode() != null ? message.getCode() : 0); - // msg - String msg = message.getMsg() != null ? message.getMsg() : ""; - byte[] msgBytes = StrUtil.utf8Bytes(msg); - bodyBuffer.appendShort((short) msgBytes.length); - bodyBuffer.appendBytes(msgBytes); - // data - if (message.getData() != null) { - bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getData())); - } - } else { - // 请求消息只处理 params 参数 - // TODO @haohao:如果为空,是不是得写个长度 0 哈? - if (message.getParams() != null) { - bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getParams())); - } - } - return bodyBuffer.getBytes(); - } - - /** - * 构建完整消息 - */ - private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) { - Buffer buffer = Buffer.buffer(); - // 1. 写入协议头部 - buffer.appendByte(MAGIC_NUMBER); - buffer.appendByte(PROTOCOL_VERSION); - buffer.appendByte(messageType); - // 2. 预留消息长度位置(在 5. 更新消息长度) - int lengthPosition = buffer.length(); - buffer.appendInt(0); - // 3. 写入消息 ID - String messageId = StrUtil.isNotBlank(message.getRequestId()) ? message.getRequestId() - : IotDeviceMessageUtils.generateMessageId(); - byte[] messageIdBytes = StrUtil.utf8Bytes(messageId); - buffer.appendShort((short) messageIdBytes.length); - buffer.appendBytes(messageIdBytes); - // 4. 写入方法名 - byte[] methodBytes = StrUtil.utf8Bytes(message.getMethod()); - buffer.appendShort((short) methodBytes.length); - buffer.appendBytes(methodBytes); - // 5. 写入消息体 - buffer.appendBytes(bodyData); - // 6. 更新消息长度 - buffer.setInt(lengthPosition, buffer.length()); - return buffer.getBytes(); - } - - /** - * 解析消息体 - */ - private IotDeviceMessage parseMessageBody(Buffer buffer, int startIndex, byte messageType, - String messageId, String method) { - if (startIndex >= buffer.length()) { - // 空消息体 - return IotDeviceMessage.of(messageId, method, null, null, null, null); - } - - if (messageType == RESPONSE) { - // 响应消息:解析 code + msg + data - return parseResponseMessage(buffer, startIndex, messageId, method); - } else { - // 请求消息:解析 payload - Object payload = parseJsonData(buffer, startIndex, buffer.length()); - return IotDeviceMessage.of(messageId, method, payload, null, null, null); - } - } - - /** - * 解析响应消息 - */ - private IotDeviceMessage parseResponseMessage(Buffer buffer, int startIndex, String messageId, String method) { - int index = startIndex; - - // 1. 读取响应码 - Integer code = buffer.getInt(index); - index += 4; - - // 2. 读取响应消息 - short msgLength = buffer.getShort(index); - index += 2; - String msg = msgLength > 0 ? buffer.getString(index, index + msgLength, StandardCharsets.UTF_8.name()) : null; - index += msgLength; - - // 3. 读取响应数据 - Object data = null; - if (index < buffer.length()) { - data = parseJsonData(buffer, index, buffer.length()); - } - - return IotDeviceMessage.of(messageId, method, null, data, code, msg); - } - - /** - * 解析 JSON 数据 - */ - private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) { - if (startIndex >= endIndex) { - return null; - } - try { - String jsonStr = buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); - return JsonUtils.parseObject(jsonStr, Object.class); - } catch (Exception e) { - log.warn("[parseJsonData][JSON 解析失败,返回原始字符串]", e); - return buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); - } - } - - /** - * 快速检测是否为二进制格式 - * - * @param data 数据 - * @return 是否为二进制格式 - */ - public static boolean isBinaryFormatQuick(byte[] data) { - return data != null && data.length >= 1 && data[0] == MAGIC_NUMBER; - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java deleted file mode 100644 index 7d62ce2e0f..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java +++ /dev/null @@ -1,110 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.stereotype.Component; - -/** - * TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器 - * - * 采用纯 JSON 格式传输,格式如下: - * { - * "id": "消息 ID", - * "method": "消息方法", - * "params": {...}, // 请求参数 - * "data": {...}, // 响应结果 - * "code": 200, // 响应错误码 - * "msg": "success", // 响应提示 - * "timestamp": 时间戳 - * } - * - * @author 芋道源码 - */ -@Component -public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { - - public static final String TYPE = "TCP_JSON"; - - @Data - @NoArgsConstructor - @AllArgsConstructor - private static class TcpJsonMessage { - - /** - * 消息 ID,且每个消息 ID 在当前设备具有唯一性 - */ - private String id; - - /** - * 请求方法 - */ - private String method; - - /** - * 请求参数 - */ - private Object params; - - /** - * 响应结果 - */ - private Object data; - - /** - * 响应错误码 - */ - private Integer code; - - /** - * 响应提示 - */ - private String msg; - - /** - * 时间戳 - */ - private Long timestamp; - - } - - @Override - public String type() { - return TYPE; - } - - @Override - public byte[] encode(IotDeviceMessage message) { - TcpJsonMessage tcpJsonMessage = new TcpJsonMessage( - message.getRequestId(), - message.getMethod(), - message.getParams(), - message.getData(), - message.getCode(), - message.getMsg(), - System.currentTimeMillis()); - return JsonUtils.toJsonByte(tcpJsonMessage); - } - - @Override - @SuppressWarnings("DataFlowIssue") - public IotDeviceMessage decode(byte[] bytes) { - String jsonStr = StrUtil.utf8Str(bytes).trim(); - TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(jsonStr, TcpJsonMessage.class); - Assert.notNull(tcpJsonMessage, "消息不能为空"); - Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); - return IotDeviceMessage.of( - tcpJsonMessage.getId(), - tcpJsonMessage.getMethod(), - tcpJsonMessage.getParams(), - tcpJsonMessage.getData(), - tcpJsonMessage.getCode(), - tcpJsonMessage.getMsg()); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java index 77f777cafa..db5ea124ee 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java @@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler { return; } // 2.2 构建载荷 - byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), + byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - // 2.3 发布消息 + + // 3. 发布消息 protocol.publishMessage(topic, payload); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java index 53705aa64e..ccc7a2b7e1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java @@ -452,16 +452,17 @@ public class IotEmqxAuthEventHandler { IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); Assert.notNull(deviceInfo, "设备信息不能为空"); try { - // 1. 构建响应消息 + // 1.1 构建响应消息 String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(null, method, result, 0, null); - - // 2. 编码消息 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, "Alink"); - - // 3. 构建响应主题并延迟发布(等待设备连接成功并完成订阅) + // 1.2 序列化消息 + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, + cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum.JSON); + // 1.3 构建响应主题 String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), true); + + // 2. 构建响应主题,并延迟发布(等待设备连接成功并完成订阅) protocol.publishDelayMessage(replyTopic, encodedData, 5000); log.info("[sendRegisterResultMessage][发送注册结果: topic={}]", replyTopic); } catch (Exception e) { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java index 4c2fa488f9..17d5f85fc0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; +import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -34,22 +35,21 @@ public class IotEmqxUpstreamHandler { try { // 1. 解析主题,一次性获取所有信息 String[] topicParts = topic.split("/"); - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { + String productKey = ArrayUtil.get(topicParts, 2); + String deviceName = ArrayUtil.get(topicParts, 3); + if (topicParts.length < 4 || StrUtil.hasBlank(productKey, deviceName)) { log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); return; } - String productKey = topicParts[2]; - String deviceName = topicParts[3]; - - // 3. 解码消息 - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); + // 2. 反序列化消息 + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); if (message == null) { log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); return; } - // 4. 发送消息到队列 + // 3. 发送消息到队列 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); } catch (Exception e) { log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java index 153da2eec1..18a5a413d2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java @@ -43,7 +43,7 @@ public class IotMqttDownstreamHandler { } // 2.1 序列化消息 - byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(), + byte[] payload = deviceMessageService.serializeDeviceMessage(message, connectionInfo.getProductKey(), connectionInfo.getDeviceName()); Assert.isTrue(payload != null && payload.length > 0, "消息编码结果不能为空"); // 2.2 构建主题 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java index 12445cb85b..443dc1069c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java @@ -21,12 +21,6 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public abstract class IotMqttAbstractHandler { - // TODO @AI:当前使用 Alink 序列化类型,后续可考虑支持更多序列化方式 - /** - * 默认编解码类型(MQTT 使用 Alink 协议) - */ - protected static final String DEFAULT_CODEC_TYPE = "Alink"; - protected final IotMqttConnectionManager connectionManager; protected final IotDeviceMessageService deviceMessageService; @@ -40,8 +34,9 @@ public abstract class IotMqttAbstractHandler { * @param method 方法名 * @param data 响应数据 */ + @SuppressWarnings("SameParameterValue") protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, - String requestId, String method, Object data) { + String requestId, String method, Object data) { IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null); writeResponse(endpoint, productKey, deviceName, method, responseMessage); } @@ -75,11 +70,12 @@ public abstract class IotMqttAbstractHandler { private void writeResponse(MqttEndpoint endpoint, String productKey, String deviceName, String method, IotDeviceMessage responseMessage) { try { - // 1. 编码消息(使用默认编解码器) - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); - - // 2. 构建响应主题,并发送 + // 1.1 序列化消息(根据设备配置的序列化类型) + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, productKey, deviceName); + // 1.2 构建响应主题 String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); + + // 2. 发送响应消息 endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); log.debug("[writeResponse][发送响应,主题: {},code: {}]", replyTopic, responseMessage.getCode()); } catch (Exception e) { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java index ec1dce2061..9640fa20b0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java @@ -14,6 +14,8 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessa import io.vertx.mqtt.MqttEndpoint; import lombok.extern.slf4j.Slf4j; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + /** * IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密) * @@ -76,7 +78,8 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { log.warn("[handleRegister][注册失败,客户端 ID: {},错误: {}]", clientId, e.getMessage()); // 接受连接,并发送错误响应 endpoint.accept(false); - sendErrorResponse(endpoint, productKey, deviceName, null, method, 500, e.getMessage()); + sendErrorResponse(endpoint, productKey, deviceName, null, method, + INTERNAL_SERVER_ERROR.getCode(), e.getMessage()); } finally { // 注册完成后关闭连接(一型一密只用于获取 deviceSecret,不保持连接) endpoint.close(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java index 4014ccdf03..00a0c4b849 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java @@ -60,7 +60,7 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配"); // 2. 反序列化消息 - message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); + message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); if (message == null) { log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); sendErrorResponse(endpoint, productKey, deviceName, null, null, diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java index 6eb414ee9f..9c8e827879 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java @@ -1,4 +1,4 @@ /** - * 提供设备接入的各种协议的实现 + * 设备接入协议:MQTT、EMQX、HTTP、TCP 等协议的实现 */ -package cn.iocoder.yudao.module.iot.gateway.protocol; \ No newline at end of file +package cn.iocoder.yudao.module.iot.gateway.protocol; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java new file mode 100644 index 0000000000..cfdda5ac02 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java @@ -0,0 +1,6 @@ +/** + * 消息序列化:将设备消息转换为字节数组(JSON、二进制等格式) + * + * @see cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer + */ +package cn.iocoder.yudao.module.iot.gateway.serialize; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java index c86fc0983d..7d16a655c2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.message; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; /** @@ -10,45 +11,45 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; public interface IotDeviceMessageService { /** - * 编码消息 + * 序列化消息 * * @param message 消息 * @param productKey 产品 Key * @param deviceName 设备名称 - * @return 编码后的消息内容 + * @return 序列化后的消息内容 */ - byte[] encodeDeviceMessage(IotDeviceMessage message, - String productKey, String deviceName); + byte[] serializeDeviceMessage(IotDeviceMessage message, + String productKey, String deviceName); /** - * 编码消息 + * 序列化消息 * - * @param message 消息 - * @param codecType 编解码器类型 - * @return 编码后的消息内容 + * @param message 消息 + * @param serializeType 序列化类型 + * @return 序列化后的消息内容 */ - byte[] encodeDeviceMessage(IotDeviceMessage message, - String codecType); + byte[] serializeDeviceMessage(IotDeviceMessage message, + IotSerializeTypeEnum serializeType); /** - * 解码消息 + * 反序列化消息 * * @param bytes 消息内容 * @param productKey 产品 Key * @param deviceName 设备名称 - * @return 解码后的消息内容 + * @return 反序列化后的消息内容 */ - IotDeviceMessage decodeDeviceMessage(byte[] bytes, - String productKey, String deviceName); + IotDeviceMessage deserializeDeviceMessage(byte[] bytes, + String productKey, String deviceName); /** - * 解码消息 + * 反序列化消息 * - * @param bytes 消息内容 - * @param codecType 编解码器类型 - * @return 解码后的消息内容 + * @param bytes 消息内容 + * @param serializeType 序列化类型 + * @return 反序列化后的消息内容 */ - IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType); + IotDeviceMessage deserializeDeviceMessage(byte[] bytes, IotSerializeTypeEnum serializeType); /** * 发送消息 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java index 014da9a5df..ee0c4aea4a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java @@ -1,20 +1,20 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.message; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS; @@ -28,80 +28,70 @@ import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVIC @Slf4j public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { - /** - * 编解码器 - */ - private final Map codes; - @Resource private IotDeviceService deviceService; @Resource private IotDeviceMessageProducer deviceMessageProducer; - public IotDeviceMessageServiceImpl(List codes) { - this.codes = CollectionUtils.convertMap(codes, IotDeviceMessageCodec::type); - } + @Resource + private IotMessageSerializerManager messageSerializerManager; @Override - public byte[] encodeDeviceMessage(IotDeviceMessage message, - String productKey, String deviceName) { + public byte[] serializeDeviceMessage(IotDeviceMessage message, + String productKey, String deviceName) { // 1.1 获取设备信息 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); if (device == null) { throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); } - // 1.2 获取编解码器 - IotDeviceMessageCodec codec = codes.get(device.getCodecType()); - if (codec == null) { - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); - } + // 1.2 获取序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType()); + Assert.notNull(serializeType, "设备序列化类型不能为空"); - // 2. 编码消息 - return codec.encode(message); + // 2. 序列化消息 + return serializeDeviceMessage(message, serializeType); } @Override - public byte[] encodeDeviceMessage(IotDeviceMessage message, - String codecType) { - // 1. 获取编解码器 - IotDeviceMessageCodec codec = codes.get(codecType); - if (codec == null) { - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + public byte[] serializeDeviceMessage(IotDeviceMessage message, + IotSerializeTypeEnum serializeType) { + // 1. 获取序列化器 + IotMessageSerializer serializer = messageSerializerManager.get(serializeType); + if (serializer == null) { + throw new IllegalArgumentException(StrUtil.format("序列化器({}) 不存在", serializeType)); } - // 2. 编码消息 - return codec.encode(message); + // 2. 序列化消息 + return serializer.serialize(message); } @Override - public IotDeviceMessage decodeDeviceMessage(byte[] bytes, - String productKey, String deviceName) { + public IotDeviceMessage deserializeDeviceMessage(byte[] bytes, + String productKey, String deviceName) { // 1.1 获取设备信息 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); if (device == null) { throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); } - // 1.2 获取编解码器 - IotDeviceMessageCodec codec = codes.get(device.getCodecType()); - if (codec == null) { - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); - } + // 1.2 获取序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType()); + Assert.notNull(serializeType, "设备序列化类型不能为空"); - // 2. 解码消息 - return codec.decode(bytes); + // 2. 反序列化消息 + return deserializeDeviceMessage(bytes, serializeType); } @Override - public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) { - // 1. 获取编解码器 - IotDeviceMessageCodec codec = codes.get(codecType); - if (codec == null) { - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + public IotDeviceMessage deserializeDeviceMessage(byte[] bytes, IotSerializeTypeEnum serializeType) { + // 1. 获取序列化器 + IotMessageSerializer serializer = messageSerializerManager.get(serializeType); + if (serializer == null) { + throw new IllegalArgumentException(StrUtil.format("序列化器({}) 不存在", serializeType)); } - // 2. 解码消息 - return codec.decode(bytes); + // 2. 反序列化消息 + return serializer.deserialize(bytes); } @Override diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java index 22ac321817..45cb7ca450 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java @@ -8,8 +8,8 @@ import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -57,9 +57,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== @@ -211,7 +211,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { client.publishHandler(message -> { log.info("[testDeviceRegister][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); responseFuture.complete(response); }); // 3.2 订阅 _reply 主题 @@ -314,14 +314,14 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { client.publishHandler(message -> { log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); responseFuture.complete(response); }); - // 2. 编码并发布消息 - byte[] payload = CODEC.encode(request); - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", - CODEC.type(), topic, new String(payload)); + // 2. 序列化并发布消息 + byte[] payload = SERIALIZER.serialize(request); + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]", + SERIALIZER.getType(), topic, new String(payload)); client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); log.info("[publishAndWaitReply][消息发布成功]"); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java index 02949c758c..8e099749c8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java @@ -12,8 +12,8 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -65,9 +65,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== @@ -399,14 +399,14 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { client.publishHandler(message -> { log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); responseFuture.complete(response); }); - // 2. 编码并发布消息 - byte[] payload = CODEC.encode(request); - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", - CODEC.type(), topic, new String(payload)); + // 2. 序列化并发布消息 + byte[] payload = SERIALIZER.serialize(request); + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]", + SERIALIZER.getType(), topic, new String(payload)); client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); log.info("[publishAndWaitReply][消息发布成功]"); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java index 5173858923..ca01b85035 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java @@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -59,9 +59,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -236,14 +236,14 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { client.publishHandler(message -> { log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); responseFuture.complete(response); }); - // 2. 编码并发布消息 - byte[] payload = CODEC.encode(request); - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", - CODEC.type(), topic, new String(payload)); + // 2. 序列化并发布消息 + byte[] payload = SERIALIZER.serialize(request); + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]", + SERIALIZER.getType(), topic, new String(payload)); client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); log.info("[publishAndWaitReply][消息发布成功]"); From eb9d27ae9e75a6093bc906a44d01623bb2693a3e Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 4 Feb 2026 18:41:31 +0800 Subject: [PATCH 30/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91SSL=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=EF=BC=8C=E7=BB=9F=E4=B8=80=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=20SslConfig=20=E6=9B=BF=E4=BB=A3=EF=BC=8C=E6=9B=B4=E7=BB=9F?= =?UTF-8?q?=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gateway/config/IotGatewayProperties.java | 60 ++++++++++++++++++- .../gateway/protocol/IotProtocolManager.java | 20 +++---- .../protocol/coap/IotCoapProtocol.java | 6 +- .../protocol/emqx/IotEmqxProtocol.java | 6 +- .../gateway/protocol/http/IotHttpConfig.java | 15 ----- .../protocol/http/IotHttpProtocol.java | 15 ++--- .../gateway/protocol/mqtt/IotMqttConfig.java | 16 ----- .../protocol/mqtt/IotMqttProtocol.java | 14 +++-- .../gateway/protocol/tcp/IotTcpConfig.java | 14 ----- .../gateway/protocol/tcp/IotTcpProtocol.java | 14 +++-- .../gateway/protocol/udp/IotUdpProtocol.java | 6 +- .../websocket/IotWebSocketConfig.java | 16 ----- .../websocket/IotWebSocketProtocol.java | 14 +++-- 13 files changed, 109 insertions(+), 107 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index d91a2f8041..6019b0f079 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -35,7 +35,7 @@ public class IotGatewayProperties { /** * 协议实例列表 */ - private List protocols; + private List protocols; @Data public static class RpcProperties { @@ -78,7 +78,7 @@ public class IotGatewayProperties { * 协议实例配置 */ @Data - public static class ProtocolInstanceProperties { + public static class ProtocolProperties { /** * 协议实例 ID,如 "http-alink"、"tcp-binary" @@ -117,6 +117,14 @@ public class IotGatewayProperties { */ private String serialize; + // ========== SSL 配置 ========== + + /** + * SSL 配置(可选,配置文件中不配置则为 null) + */ + @Valid + private SslConfig ssl; + // ========== 各协议配置 ========== /** @@ -160,4 +168,52 @@ public class IotGatewayProperties { } + /** + * SSL 配置 + */ + @Data + public static class SslConfig { + + /** + * 是否启用 SSL + */ + @NotNull(message = "是否启用 SSL 不能为空") + private Boolean ssl = false; + + /** + * SSL 证书路径 + */ + @NotEmpty(message = "SSL 证书路径不能为空") + private String sslCertPath; + + /** + * SSL 私钥路径 + */ + @NotEmpty(message = "SSL 私钥路径不能为空") + private String sslKeyPath; + + /** + * 密钥库(KeyStore)路径 + *

                              + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证) + */ + private String keyStorePath; + /** + * 密钥库密码 + */ + private String keyStorePassword; + + /** + * 信任库(TrustStore)路径 + *

                              + * 包含服务端信任的 CA 证书,用于验证服务端的身份 + */ + private String trustStorePath; + /** + * 信任库密码 + */ + private String trustStorePassword; + + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index 5d1b6c6d7f..3cd00c7573 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -45,13 +45,13 @@ public class IotProtocolManager implements SmartLifecycle { if (running) { return; } - List protocolConfigs = gatewayProperties.getProtocols(); + List protocolConfigs = gatewayProperties.getProtocols(); if (CollUtil.isEmpty(protocolConfigs)) { log.info("[start][没有配置协议实例,跳过启动]"); return; } - for (IotGatewayProperties.ProtocolInstanceProperties config : protocolConfigs) { + for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) { if (BooleanUtil.isFalse(config.getEnabled())) { log.info("[start][协议实例 {} 未启用,跳过]", config.getId()); continue; @@ -91,7 +91,7 @@ public class IotProtocolManager implements SmartLifecycle { * @return 协议实例 */ @SuppressWarnings({"EnhancedSwitchMigration"}) - private IotProtocol createProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotProtocol createProtocol(IotGatewayProperties.ProtocolProperties config) { IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol()); if (protocolType == null) { log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol()); @@ -124,7 +124,7 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return HTTP 协议实例 */ - private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) { return new IotHttpProtocol(config); } @@ -134,7 +134,7 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return TCP 协议实例 */ - private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) { return new IotTcpProtocol(config); } @@ -144,7 +144,7 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return UDP 协议实例 */ - private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) { return new IotUdpProtocol(config); } @@ -154,7 +154,7 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return CoAP 协议实例 */ - private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) { return new IotCoapProtocol(config); } @@ -164,7 +164,7 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return WebSocket 协议实例 */ - private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) { return new IotWebSocketProtocol(config); } @@ -174,7 +174,7 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return MQTT 协议实例 */ - private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolProperties config) { return new IotMqttProtocol(config); } @@ -184,7 +184,7 @@ public class IotProtocolManager implements SmartLifecycle { * @param config 协议实例配置 * @return EMQX 协议实例 */ - private IotEmqxProtocol createEmqxProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { + private IotEmqxProtocol createEmqxProtocol(IotGatewayProperties.ProtocolProperties config) { return new IotEmqxProtocol(config); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java index ac348a2db5..14fe10dcd8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java @@ -4,7 +4,7 @@ import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthHandler; @@ -43,7 +43,7 @@ public class IotCoapProtocol implements IotProtocol { /** * 协议配置 */ - private final ProtocolInstanceProperties properties; + private final ProtocolProperties properties; /** * 服务器 ID(用于消息追踪,全局唯一) */ @@ -66,7 +66,7 @@ public class IotCoapProtocol implements IotProtocol { */ private final IotCoapDownstreamSubscriber downstreamSubscriber; - public IotCoapProtocol(ProtocolInstanceProperties properties) { + public IotCoapProtocol(ProtocolProperties properties) { IotCoapConfig coapConfig = properties.getCoap(); Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); this.properties = properties; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java index 0d64186aa5..a9e251736f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java @@ -7,7 +7,7 @@ import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler; @@ -47,7 +47,7 @@ public class IotEmqxProtocol implements IotProtocol { /** * 协议配置 */ - private final ProtocolInstanceProperties properties; + private final ProtocolProperties properties; /** * EMQX 配置 */ @@ -92,7 +92,7 @@ public class IotEmqxProtocol implements IotProtocol { */ private final IotEmqxDownstreamSubscriber downstreamSubscriber; - public IotEmqxProtocol(ProtocolInstanceProperties properties) { + public IotEmqxProtocol(ProtocolProperties properties) { Assert.notNull(properties, "协议实例配置不能为空"); Assert.notNull(properties.getEmqx(), "EMQX 协议配置(emqx)不能为空"); this.properties = properties; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java index 968a9ae625..bc7f4dc8c5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java @@ -10,19 +10,4 @@ import lombok.Data; @Data public class IotHttpConfig { - /** - * 是否启用 SSL - */ - private Boolean sslEnabled = false; - - /** - * SSL 证书路径 - */ - private String sslCertPath; - - /** - * SSL 私钥路径 - */ - private String sslKeyPath; - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java index 164c06f3e9..2f92419161 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java @@ -4,7 +4,8 @@ import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler; @@ -33,7 +34,7 @@ public class IotHttpProtocol implements IotProtocol { /** * 协议配置 */ - private final ProtocolInstanceProperties properties; + private final ProtocolProperties properties; /** * 服务器 ID(用于消息追踪,全局唯一) */ @@ -60,7 +61,7 @@ public class IotHttpProtocol implements IotProtocol { */ private IotHttpDownstreamSubscriber downstreamSubscriber; - public IotHttpProtocol(ProtocolInstanceProperties properties) { + public IotHttpProtocol(ProtocolProperties properties) { this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); @@ -104,12 +105,12 @@ public class IotHttpProtocol implements IotProtocol { router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); // 1.4 启动 HTTP 服务器 - IotHttpConfig httpConfig = properties.getHttp(); HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); - if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) { + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(httpConfig.getSslKeyPath()) - .setCertPath(httpConfig.getSslCertPath()); + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); } try { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java index 416dcced66..afbdd93b36 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java @@ -26,20 +26,4 @@ public class IotMqttConfig { @Min(value = 1, message = "连接超时时间不能小于 1 秒") private Integer connectTimeoutSeconds = 60; - /** - * 是否启用 SSL - */ - @NotNull(message = "是否启用 SSL 不能为空") - private Boolean sslEnabled = false; - - /** - * SSL 证书路径 - */ - private String sslCertPath; - - /** - * SSL 私钥路径 - */ - private String sslKeyPath; - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java index 85d21853ef..1201fd1a42 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java @@ -8,7 +8,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber; @@ -51,7 +52,7 @@ public class IotMqttProtocol implements IotProtocol { /** * 协议配置 */ - private final ProtocolInstanceProperties properties; + private final ProtocolProperties properties; /** * 服务器 ID(用于消息追踪,全局唯一) */ @@ -88,7 +89,7 @@ public class IotMqttProtocol implements IotProtocol { private final IotMqttRegisterHandler registerHandler; private final IotMqttUpstreamHandler upstreamHandler; - public IotMqttProtocol(ProtocolInstanceProperties properties) { + public IotMqttProtocol(ProtocolProperties properties) { IotMqttConfig mqttConfig = properties.getMqtt(); Assert.notNull(mqttConfig, "MQTT 协议配置(mqtt)不能为空"); this.properties = properties; @@ -136,10 +137,11 @@ public class IotMqttProtocol implements IotProtocol { .setPort(properties.getPort()) .setMaxMessageSize(mqttConfig.getMaxMessageSize()) .setTimeoutOnConnect(mqttConfig.getConnectTimeoutSeconds()); - if (Boolean.TRUE.equals(mqttConfig.getSslEnabled())) { + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(mqttConfig.getSslKeyPath()) - .setCertPath(mqttConfig.getSslCertPath()); + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java index 16dd3b50e5..a8c21c8719 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java @@ -27,20 +27,6 @@ public class IotTcpConfig { @Min(value = 1000, message = "心跳超时时间必须大于 1000 毫秒") private Long keepAliveTimeoutMs = 30000L; - /** - * 是否启用 SSL - */ - @NotNull(message = "是否启用 SSL 不能为空") - private Boolean sslEnabled = false; - /** - * SSL 证书路径 - */ - private String sslCertPath; - /** - * SSL 私钥路径 - */ - private String sslKeyPath; - /** * 拆包配置 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java index 24660389b7..e864df543e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java @@ -5,7 +5,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; @@ -36,7 +37,7 @@ public class IotTcpProtocol implements IotProtocol { /** * 协议配置 */ - private final ProtocolInstanceProperties properties; + private final ProtocolProperties properties; /** * 服务器 ID(用于消息追踪,全局唯一) */ @@ -76,7 +77,7 @@ public class IotTcpProtocol implements IotProtocol { */ private final IotTcpFrameCodec frameCodec; - public IotTcpProtocol(ProtocolInstanceProperties properties) { + public IotTcpProtocol(ProtocolProperties properties) { IotTcpConfig tcpConfig = properties.getTcp(); Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); @@ -128,10 +129,11 @@ public class IotTcpProtocol implements IotProtocol { .setTcpNoDelay(true) .setReuseAddress(true) .setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时 - if (Boolean.TRUE.equals(tcpConfig.getSslEnabled())) { + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(tcpConfig.getSslKeyPath()) - .setCertPath(tcpConfig.getSslCertPath()); + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java index 13cd85b0ed..bfed2d9c32 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java @@ -4,7 +4,7 @@ import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber; @@ -33,7 +33,7 @@ public class IotUdpProtocol implements IotProtocol { /** * 协议配置 */ - private final ProtocolInstanceProperties properties; + private final ProtocolProperties properties; /** * 服务器 ID(用于消息追踪,全局唯一) */ @@ -70,7 +70,7 @@ public class IotUdpProtocol implements IotProtocol { */ private final IotMessageSerializer serializer; - public IotUdpProtocol(ProtocolInstanceProperties properties) { + public IotUdpProtocol(ProtocolProperties properties) { IotUdpConfig udpConfig = properties.getUdp(); Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); this.properties = properties; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java index e64e11dc51..95f738d0a1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java @@ -35,20 +35,4 @@ public class IotWebSocketConfig { @NotNull(message = "空闲超时时间不能为空") private Integer idleTimeoutSeconds = 60; - /** - * 是否启用 SSL(wss://) - */ - @NotNull(message = "是否启用 SSL 不能为空") - private Boolean sslEnabled = false; - - /** - * SSL 证书路径 - */ - private String sslCertPath; - - /** - * SSL 私钥路径 - */ - private String sslKeyPath; - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java index 10a57f9b99..b416900db5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java @@ -6,7 +6,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler; @@ -35,7 +36,7 @@ public class IotWebSocketProtocol implements IotProtocol { /** * 协议配置 */ - private final ProtocolInstanceProperties properties; + private final ProtocolProperties properties; /** * 服务器 ID(用于消息追踪,全局唯一) */ @@ -71,7 +72,7 @@ public class IotWebSocketProtocol implements IotProtocol { */ private final IotMessageSerializer serializer; - public IotWebSocketProtocol(ProtocolInstanceProperties properties) { + public IotWebSocketProtocol(ProtocolProperties properties) { Assert.notNull(properties, "协议实例配置不能为空"); Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空"); this.properties = properties; @@ -120,10 +121,11 @@ public class IotWebSocketProtocol implements IotProtocol { .setIdleTimeout(wsConfig.getIdleTimeoutSeconds()) .setMaxWebSocketFrameSize(wsConfig.getMaxFrameSize()) .setMaxWebSocketMessageSize(wsConfig.getMaxMessageSize()); - if (Boolean.TRUE.equals(wsConfig.getSslEnabled())) { + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(wsConfig.getSslKeyPath()) - .setCertPath(wsConfig.getSslCertPath()); + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); } From 4319220750a142bf62c5ae42baa99472708be047 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 6 Feb 2026 00:18:42 +0800 Subject: [PATCH 31/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E3=80=90=E5=8D=8F=E8=AE=AE=E6=94=B9=E9=80=A0=E3=80=91modbus-tc?= =?UTF-8?q?p=EF=BC=9A=E6=94=B9=E9=80=A0=E6=96=B0=E7=9A=84=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/IotGatewayConfiguration.java | 124 +--------- .../gateway/config/IotGatewayProperties.java | 34 +-- .../gateway/protocol/IotProtocolManager.java | 13 ++ .../modbustcp/IotModbusTcpConfig.java | 22 ++ .../modbustcp/IotModbusTcpProtocol.java | 215 ++++++++++++++++++ .../IotModbusTcpUpstreamProtocol.java | 115 ---------- .../IotModbusTcpDownstreamHandler.java | 10 +- .../IotModbusTcpDownstreamSubscriber.java | 25 +- .../IotModbusTcpUpstreamHandler.java | 15 +- .../manager/IotModbusTcpPollScheduler.java | 2 +- .../src/main/resources/application.yaml | 48 +--- 11 files changed, 300 insertions(+), 323 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfig.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/{router => handler/downstream}/IotModbusTcpDownstreamHandler.java (94%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/{ => handler/downstream}/IotModbusTcpDownstreamSubscriber.java (69%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/{router => handler/upstream}/IotModbusTcpUpstreamHandler.java (85%) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index 2f865d983b..5c2fd860e9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -1,48 +1,16 @@ package cn.iocoder.yudao.module.iot.gateway.config; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.*; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import lombok.extern.slf4j.Slf4j; -import org.redisson.api.RedissonClient; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import lombok.extern.slf4j.Slf4j; -import org.redisson.api.RedissonClient; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +/** + * IoT 网关配置类 + * + * @author 芋道源码 + */ @Configuration @EnableConfigurationProperties(IotGatewayProperties.class) public class IotGatewayConfiguration { @@ -57,86 +25,4 @@ public class IotGatewayConfiguration { return new IotProtocolManager(gatewayProperties); } - /** - * IoT 网关 Modbus TCP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.modbus-tcp", name = "enabled", havingValue = "true") - @Slf4j - public static class ModbusTcpProtocolConfiguration { - - @Bean(name = "modbusTcpVertx", destroyMethod = "close") - public Vertx modbusTcpVertx() { - return Vertx.vertx(); - } - - @Bean - public IotModbusDataConverter iotModbusDataConverter() { - return new IotModbusDataConverter(); - } - - @Bean - public IotModbusTcpClient iotModbusTcpClient() { - return new IotModbusTcpClient(); - } - - @Bean - public IotModbusTcpConnectionManager iotModbusTcpConnectionManager( - RedissonClient redissonClient, - @Qualifier("modbusTcpVertx") Vertx modbusTcpVertx) { - return new IotModbusTcpConnectionManager(redissonClient, modbusTcpVertx); - } - - @Bean - public IotModbusTcpConfigCacheService iotModbusTcpConfigCacheService( - cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi deviceApi) { - return new IotModbusTcpConfigCacheService(deviceApi); - } - - @Bean - public IotModbusTcpUpstreamHandler iotModbusTcpUpstreamHandler( - IotDeviceMessageService messageService, - IotModbusDataConverter dataConverter) { - return new IotModbusTcpUpstreamHandler(messageService, dataConverter); - } - - @Bean - public IotModbusTcpPollScheduler iotModbusTcpPollScheduler( - @Qualifier("modbusTcpVertx") Vertx modbusTcpVertx, - IotModbusTcpConnectionManager connectionManager, - IotModbusTcpClient modbusClient, - IotModbusTcpUpstreamHandler upstreamHandler) { - return new IotModbusTcpPollScheduler(modbusTcpVertx, connectionManager, modbusClient, upstreamHandler); - } - - @Bean - public IotModbusTcpDownstreamHandler iotModbusTcpDownstreamHandler( - IotModbusTcpConnectionManager connectionManager, - IotModbusTcpClient modbusClient, - IotModbusDataConverter dataConverter, - IotModbusTcpConfigCacheService configCacheService) { - return new IotModbusTcpDownstreamHandler(connectionManager, modbusClient, dataConverter, configCacheService); - } - - @Bean - public IotModbusTcpUpstreamProtocol iotModbusTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceMessageService messageService, - IotModbusTcpConnectionManager connectionManager, - IotModbusTcpPollScheduler pollScheduler, - IotModbusTcpConfigCacheService configCacheService, - IotModbusTcpUpstreamHandler upstreamHandler, - @Qualifier("modbusTcpVertx") Vertx modbusTcpVertx) { - return new IotModbusTcpUpstreamProtocol(gatewayProperties.getProtocol().getModbusTcp(), - messageService, connectionManager, pollScheduler, configCacheService, upstreamHandler, modbusTcpVertx); - } - - @Bean - public IotModbusTcpDownstreamSubscriber iotModbusTcpDownstreamSubscriber(IotModbusTcpUpstreamProtocol upstreamProtocol, - IotModbusTcpDownstreamHandler downstreamHandler, - IotMessageBus messageBus) { - return new IotModbusTcpDownstreamSubscriber(upstreamProtocol, downstreamHandler, messageBus); - } - - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index ce0222c980..8636bbd061 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -4,6 +4,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; @@ -166,6 +167,12 @@ public class IotGatewayProperties { @Valid private IotEmqxConfig emqx; + /** + * Modbus TCP 协议配置 + */ + @Valid + private IotModbusTcpConfig modbusTcp; + } /** @@ -216,31 +223,4 @@ public class IotGatewayProperties { } - @Data - public static class ProtocolProperties { - - /** - * Modbus TCP 组件配置 - */ - private ModbusTcpProperties modbusTcp; - - } - - @Data - public static class ModbusTcpProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 配置刷新间隔(秒) - */ - @NotNull(message = "配置刷新间隔不能为空") - private Integer configRefreshInterval = 30; - - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index 3cd00c7573..1549d7d23d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; @@ -112,6 +113,8 @@ public class IotProtocolManager implements SmartLifecycle { return createMqttProtocol(config); case EMQX: return createEmqxProtocol(config); + case MODBUS_TCP: + return createModbusTcpProtocol(config); default: throw new IllegalArgumentException(String.format( "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); @@ -188,4 +191,14 @@ public class IotProtocolManager implements SmartLifecycle { return new IotEmqxProtocol(config); } + /** + * 创建 Modbus TCP 协议实例 + * + * @param config 协议实例配置 + * @return Modbus TCP 协议实例 + */ + private IotModbusTcpProtocol createModbusTcpProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotModbusTcpProtocol(config); + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfig.java new file mode 100644 index 0000000000..5893869e27 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfig.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT Modbus TCP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotModbusTcpConfig { + + /** + * 配置刷新间隔(秒) + */ + @NotNull(message = "配置刷新间隔不能为空") + @Min(value = 1, message = "配置刷新间隔不能小于 1 秒") + private Integer configRefreshInterval = 30; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpProtocol.java new file mode 100644 index 0000000000..163e1e5fb7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpProtocol.java @@ -0,0 +1,215 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream.IotModbusTcpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream.IotModbusTcpDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.upstream.IotModbusTcpUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * IoT 网关 Modbus TCP 协议:主动轮询 Modbus 从站设备数据 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private final Vertx vertx; + /** + * 配置刷新定时器 ID + */ + private Long configRefreshTimerId; + + /** + * 连接管理器 + */ + private final IotModbusTcpConnectionManager connectionManager; + /** + * 下行消息订阅者 + */ + private final IotModbusTcpDownstreamSubscriber downstreamSubscriber; + + private final IotModbusTcpConfigCacheService configCacheService; + private final IotModbusTcpPollScheduler pollScheduler; + + public IotModbusTcpProtocol(ProtocolProperties properties) { + IotModbusTcpConfig modbusTcpConfig = properties.getModbusTcp(); + Assert.notNull(modbusTcpConfig, "Modbus TCP 协议配置(modbusTcp)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化 Vertx + this.vertx = Vertx.vertx(); + + // 初始化 Manager + RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class); + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.connectionManager = new IotModbusTcpConnectionManager(redissonClient, vertx); + this.configCacheService = new IotModbusTcpConfigCacheService(deviceApi); + + // 初始化 Handler + IotModbusDataConverter dataConverter = new IotModbusDataConverter(); + IotModbusTcpClient modbusClient = new IotModbusTcpClient(); + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); + IotModbusTcpUpstreamHandler upstreamHandler = new IotModbusTcpUpstreamHandler(messageService, dataConverter, serverId); + IotModbusTcpDownstreamHandler downstreamHandler = new IotModbusTcpDownstreamHandler(connectionManager, + modbusClient, dataConverter, configCacheService); + + // 初始化轮询调度器 + this.pollScheduler = new IotModbusTcpPollScheduler(vertx, connectionManager, modbusClient, upstreamHandler); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotModbusTcpDownstreamSubscriber(this, downstreamHandler, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.MODBUS_TCP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT Modbus TCP 协议 {} 已经在运行中]", getId()); + return; + } + + try { + // 1.1 首次加载配置 + refreshConfig(); + // 1.2 启动配置刷新定时器 + int refreshInterval = properties.getModbusTcp().getConfigRefreshInterval(); + configRefreshTimerId = vertx.setPeriodic( + TimeUnit.SECONDS.toMillis(refreshInterval), + id -> refreshConfig() + ); + running = true; + log.info("[start][IoT Modbus TCP 协议 {} 启动成功,serverId={}]", getId(), serverId); + + // 2. 启动下行消息订阅者 + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT Modbus TCP 协议 {} 启动失败]", getId(), e); + // 启动失败时关闭资源 + if (vertx != null) { + vertx.close(); + } + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + // 1. 停止下行消息订阅者 + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT Modbus TCP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT Modbus TCP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + + // 2.1 取消配置刷新定时器 + if (configRefreshTimerId != null) { + vertx.cancelTimer(configRefreshTimerId); + configRefreshTimerId = null; + } + // 2.2 停止轮询调度器 + pollScheduler.stopAll(); + log.info("[stop][IoT Modbus TCP 协议 {} 轮询调度器已停止]", getId()); + // 2.3 关闭所有连接 + connectionManager.closeAll(); + log.info("[stop][IoT Modbus TCP 协议 {} 连接管理器已关闭]", getId()); + + // 3. 关闭 Vert.x 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT Modbus TCP 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT Modbus TCP 协议 {} Vertx 关闭失败]", getId(), e); + } + } + running = false; + log.info("[stop][IoT Modbus TCP 协议 {} 已停止]", getId()); + } + + /** + * 刷新配置 + */ + private synchronized void refreshConfig() { + try { + // 1. 从 biz 拉取最新配置 + List configs = configCacheService.refreshConfig(); + log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); + + // 2. 更新连接和轮询任务 + for (IotModbusDeviceConfigRespDTO config : configs) { + try { + // 2.1 确保连接存在 + connectionManager.ensureConnection(config); + // 2.2 更新轮询任务 + pollScheduler.updatePolling(config); + } catch (Exception e) { + log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); + } + } + + // 3. 清理已删除设备的资源 + configCacheService.cleanupRemovedDevices(configs, deviceId -> { + pollScheduler.stopPolling(deviceId); + connectionManager.removeDevice(deviceId); + }); + } catch (Exception e) { + log.error("[refreshConfig][刷新配置失败]", e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java deleted file mode 100644 index 1d47d30de2..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java +++ /dev/null @@ -1,115 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; - -import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -/** - * IoT Modbus TCP 上行协议:定时拉取配置、管理连接、调度轮询任务 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class IotModbusTcpUpstreamProtocol { - - private final IotGatewayProperties.ModbusTcpProperties modbusTcpProperties; - private final IotDeviceMessageService messageService; - private final IotModbusTcpConnectionManager connectionManager; - private final IotModbusTcpPollScheduler pollScheduler; - private final IotModbusTcpConfigCacheService configCacheService; - private final IotModbusTcpUpstreamHandler upstreamHandler; - private final Vertx vertx; - - /** - * 服务器 ID,用于标识当前网关实例 - */ - @Getter - private final String serverId = UUID.randomUUID().toString(); - - /** - * 配置刷新定时器 ID - */ - private Long configRefreshTimerId; - - @PostConstruct - public void start() { - log.info("[start][Modbus TCP 协议启动, serverId={}]", serverId); - - // 0. 设置 serverId 到上行处理器 - upstreamHandler.setServerId(serverId); - - // 1. 首次加载配置 - refreshConfig(); - - // 2. 启动配置定时刷新 - int refreshInterval = modbusTcpProperties.getConfigRefreshInterval(); - configRefreshTimerId = vertx.setPeriodic( - TimeUnit.SECONDS.toMillis(refreshInterval), - id -> refreshConfig() - ); - log.info("[start][配置刷新定时器已启动, 间隔={}秒]", refreshInterval); - } - - @PreDestroy - public void stop() { - log.info("[stop][Modbus TCP 协议停止]"); - - // 1. 取消配置刷新定时器 - if (configRefreshTimerId != null) { - vertx.cancelTimer(configRefreshTimerId); - } - - // 2. 停止轮询调度器 - pollScheduler.stopAll(); - - // 3. 关闭所有连接 - connectionManager.closeAll(); - } - - /** - * 刷新配置 - */ - private void refreshConfig() { - try { - // 1. 从 biz 拉取最新配置 - List configs = configCacheService.refreshConfig(); - log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); - - // 2. 更新连接和轮询任务 - for (IotModbusDeviceConfigRespDTO config : configs) { - try { - // 2.1 确保连接存在 - connectionManager.ensureConnection(config); - // 2.2 更新轮询任务 - pollScheduler.updatePolling(config); - } catch (Exception e) { - log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); - } - } - - // 3. 清理已删除设备的资源 - configCacheService.cleanupRemovedDevices(configs, deviceId -> { - pollScheduler.stopPolling(deviceId); - connectionManager.removeDevice(deviceId); - }); - } catch (Exception e) { - log.error("[refreshConfig][刷新配置失败]", e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/router/IotModbusTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamHandler.java similarity index 94% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/router/IotModbusTcpDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamHandler.java index 2355a3d8ec..49c3931fed 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/router/IotModbusTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamHandler.java @@ -1,12 +1,13 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService; import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; import lombok.RequiredArgsConstructor; @@ -16,7 +17,7 @@ import java.util.Map; /** * IoT Modbus TCP 下行消息处理器 - * + *

                              * 负责: * 1. 处理下行消息(如属性设置 thing.service.property.set) * 2. 执行 Modbus 写入操作 @@ -38,8 +39,7 @@ public class IotModbusTcpDownstreamHandler { @SuppressWarnings("unchecked") public void handle(IotDeviceMessage message) { // 1.1 检查是否是属性设置消息 - // TODO @AI:要使用枚举 - if (!"thing.service.property.set".equals(message.getMethod())) { + if (!IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod().equals(message.getMethod())) { log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); return; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamSubscriber.java similarity index 69% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamSubscriber.java index a05101329c..bea82919ef 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamSubscriber.java @@ -1,11 +1,10 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpDownstreamHandler; -import jakarta.annotation.PostConstruct; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpProtocol; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -18,19 +17,29 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber { - private final IotModbusTcpUpstreamProtocol upstreamProtocol; + private final IotModbusTcpProtocol protocol; private final IotModbusTcpDownstreamHandler downstreamHandler; private final IotMessageBus messageBus; - @PostConstruct - public void subscribe() { + /** + * 启动订阅 + */ + public void start() { messageBus.register(this); - log.info("[subscribe][Modbus TCP 下行消息订阅器已启动, topic={}]", getTopic()); + log.info("[start][Modbus TCP 下行消息订阅器已启动, topic={}]", getTopic()); + } + + /** + * 停止订阅 + */ + public void stop() { + messageBus.unregister(this); + log.info("[stop][Modbus TCP 下行消息订阅器已停止]"); } @Override public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId()); + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); } @Override diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/router/IotModbusTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/upstream/IotModbusTcpUpstreamHandler.java similarity index 85% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/router/IotModbusTcpUpstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/upstream/IotModbusTcpUpstreamHandler.java index 5da669e46f..adffc3dcc2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/router/IotModbusTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/upstream/IotModbusTcpUpstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.upstream; import cn.hutool.core.map.MapUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; @@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import java.util.Map; @@ -22,14 +21,14 @@ public class IotModbusTcpUpstreamHandler { private final IotDeviceMessageService messageService; private final IotModbusDataConverter dataConverter; - - @Setter - private String serverId; + private final String serverId; public IotModbusTcpUpstreamHandler(IotDeviceMessageService messageService, - IotModbusDataConverter dataConverter) { + IotModbusDataConverter dataConverter, + String serverId) { this.messageService = messageService; this.dataConverter = dataConverter; + this.serverId = serverId; } /** @@ -40,8 +39,8 @@ public class IotModbusTcpUpstreamHandler { * @param rawValue 原始值(int 数组) */ public void handleReadResult(IotModbusDeviceConfigRespDTO config, - IotModbusPointRespDTO point, - int[] rawValue) { + IotModbusPointRespDTO point, + int[] rawValue) { try { // 1.1 转换原始值为物模型属性值 Object convertedValue = dataConverter.convertToPropertyValue(rawValue, point); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpPollScheduler.java index 3caf030ddf..f12ff1de2a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpPollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpPollScheduler.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.upstream.IotModbusTcpUpstreamHandler; import io.vertx.core.Vertx; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index b4f4226b1a..77323e03b1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -51,8 +51,6 @@ yudao: protocol: http port: 8092 enabled: false - http: - ssl-enabled: false # ==================================== # 针对引入的 TCP 组件的配置 # ==================================== @@ -64,7 +62,6 @@ yudao: tcp: max-connections: 1000 keep-alive-timeout-ms: 30000 - ssl-enabled: false codec: type: delimiter # 拆包类型:length_field / delimiter / fixed_length delimiter: "\\n" # 分隔符(支持转义:\\n=换行, \\r=回车, \\t=制表符) @@ -101,7 +98,6 @@ yudao: max-message-size: 65536 # 最大消息大小(字节,默认 64KB) max-frame-size: 65536 # 最大帧大小(字节,默认 64KB) idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60) - ssl-enabled: false # 是否启用 SSL(wss://) # ==================================== # 针对引入的 CoAP 组件的配置 # ==================================== @@ -117,14 +113,13 @@ yudao: # 针对引入的 MQTT 组件的配置 # ==================================== - id: mqtt-json - enabled: true + enabled: false protocol: mqtt port: 1883 serialize: json mqtt: max-message-size: 8192 # 最大消息大小(字节) connect-timeout-seconds: 60 # 连接超时时间(秒) - ssl-enabled: false # 是否启用 SSL # ==================================== # 针对引入的 EMQX 组件的配置 # ==================================== @@ -168,43 +163,15 @@ yudao: key-store-password: "your-keystore-password" # 客户端证书库密码 trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 - http-port: 8090 # MQTT HTTP 服务端口 - mqtt-host: 127.0.0.1 # MQTT Broker 地址 - mqtt-port: 1883 # MQTT Broker 端口 - mqtt-username: admin # MQTT 用户名 - mqtt-password: public # MQTT 密码 - mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID - mqtt-ssl: false # 是否开启 SSL - mqtt-topics: - - "/sys/#" # 系统主题 - clean-session: true # 是否启用 Clean Session (默认: true) - keep-alive-interval-seconds: 60 # 心跳间隔,单位秒 (默认: 60) - max-inflight-queue: 10000 # 最大飞行消息队列,单位:条 - connect-timeout-seconds: 10 # 连接超时,单位:秒 - # 是否信任所有 SSL 证书 (默认: false)。警告:生产环境必须为 false! - # 仅在开发环境或内网测试时,如果使用了自签名证书,可以临时设置为 true - trust-all: true # 在 dev 环境可以设为 true - # 遗嘱消息配置 (用于网关异常下线时通知其他系统) - will: - enabled: true # 生产环境强烈建议开启 - topic: "gateway/status/${yudao.iot.gateway.emqx.mqtt-client-id}" # 遗嘱消息主题 - payload: "offline" # 遗嘱消息负载 - qos: 1 # 遗嘱消息 QoS - retain: true # 遗嘱消息是否保留 - # 高级 SSL/TLS 配置 (当 trust-all: false 且 mqtt-ssl: true 时生效) - ssl-options: - key-store-path: "classpath:certs/client.jks" # 客户端证书库路径 - key-store-password: "your-keystore-password" # 客户端证书库密码 - trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 - trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 # ==================================== - # 针对引入的 MQTT 组件的配置 + # 针对引入的 Modbus TCP 组件的配置 # ==================================== - # 协议配置(旧版,保持兼容) - protocol: - modbus-tcp: + - id: modbus-tcp-1 enabled: true - config-refresh-interval: 30 # 配置刷新间隔(秒) + protocol: modbus_tcp + port: 502 + modbus-tcp: + config-refresh-interval: 30 # 配置刷新间隔(秒) --- #################### 日志相关配置 #################### @@ -226,6 +193,7 @@ logging: cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG + cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp: DEBUG # 根日志级别 root: INFO From 2ef7612fc2bd06d31a4fd3b3e58e2a895da505cd Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 7 Feb 2026 17:42:50 +0800 Subject: [PATCH 32/53] =?UTF-8?q?fix=EF=BC=88system=EF=BC=89:=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20getPermissionInfo=20=E5=9B=A0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=9D=83=E9=99=90=E8=BF=87=E6=BB=A4=E5=AF=BC=E8=87=B4=E6=9F=A5?= =?UTF-8?q?=E4=B8=8D=E5=88=B0=E7=94=A8=E6=88=B7=EF=BC=8C=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/system/controller/admin/auth/AuthController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java index e41aee576d..6f95673c4b 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java @@ -91,6 +91,7 @@ public class AuthController { @GetMapping("/get-permission-info") @Operation(summary = "获取登录用户的权限信息") + @DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致无法查询用户。类似:https://t.zsxq.com/LHnrp public CommonResult getPermissionInfo() { // 1.1 获得用户信息 AdminUserDO user = userService.getUser(getLoginUserId()); From a0db86848d112820a80c095725c078693ee13945 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 7 Feb 2026 23:04:03 +0800 Subject: [PATCH 33/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A1?= =?UTF-8?q?=EF=BC=89=E9=87=8D=E6=9E=84=20modbus=20tcp=20=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E7=9A=84=E5=AE=9E=E7=8E=B0=E4=B8=BA=20modbus-tcp-master?= =?UTF-8?q?=EF=BC=9B2=EF=BC=89=E6=96=B0=E5=A2=9E=20modbus-tcp-slave?= =?UTF-8?q?=E3=80=90=E5=88=9D=E6=AD=A5=E5=AE=9E=E7=8E=B0=EF=BC=8C=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=87=86=E5=A4=87=E4=BC=98=E5=8C=96=E3=80=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modbus/IotDeviceModbusConfigRespVO.java | 8 + .../IotDeviceModbusConfigSaveReqVO.java | 8 + .../device/IotDeviceModbusConfigDO.java | 20 +- .../biz/dto/IotModbusDeviceConfigRespDTO.java | 10 +- .../core/enums/IotModbusFrameFormatEnum.java | 39 ++ .../iot/core/enums/IotModbusModeEnum.java | 39 ++ .../iot/core/enums/IotProtocolTypeEnum.java | 3 +- .../gateway/config/IotGatewayProperties.java | 13 +- .../gateway/protocol/IotProtocolManager.java | 27 +- .../common}/IotModbusDataConverter.java | 2 +- .../tcpmaster/IotModbusTcpMasterConfig.java} | 6 +- .../IotModbusTcpMasterProtocol.java} | 52 +-- .../tcpmaster}/client/IotModbusTcpClient.java | 5 +- .../IotModbusTcpDownstreamHandler.java | 10 +- .../IotModbusTcpDownstreamSubscriber.java | 11 +- .../upstream/IotModbusTcpUpstreamHandler.java | 4 +- .../IotModbusTcpConfigCacheService.java | 2 +- .../IotModbusTcpConnectionManager.java | 2 +- .../manager/IotModbusTcpPollScheduler.java | 7 +- .../tcpmaster}/package-info.java | 4 +- .../tcpslave/IotModbusTcpSlaveConfig.java | 43 ++ .../tcpslave/IotModbusTcpSlaveProtocol.java | 372 ++++++++++++++++++ .../modbus/tcpslave/codec/IotModbusFrame.java | 49 +++ .../tcpslave/codec/IotModbusFrameCodec.java | 317 +++++++++++++++ .../codec/IotModbusRecordParserFactory.java | 362 +++++++++++++++++ .../codec/IotModbusResponseParser.java | 80 ++++ .../IotModbusTcpSlaveDownstreamHandler.java | 152 +++++++ ...IotModbusTcpSlaveDownstreamSubscriber.java | 61 +++ .../IotModbusTcpSlaveUpstreamHandler.java | 312 +++++++++++++++ .../IotModbusTcpSlaveConfigCacheService.java | 172 ++++++++ .../IotModbusTcpSlaveConnectionManager.java | 160 ++++++++ ...otModbusTcpSlavePendingRequestManager.java | 151 +++++++ .../IotModbusTcpSlavePollScheduler.java | 207 ++++++++++ .../src/main/resources/application.yaml | 24 +- .../protocol/modbus/ModbusRtuOverTcpDemo.java | 304 ++++++++++++++ .../ModbusTcpSlaveSimulatorTest.java | 2 +- ...odbusTcpSlaveModbusRtuIntegrationTest.java | 309 +++++++++++++++ ...odbusTcpSlaveModbusTcpIntegrationTest.java | 334 ++++++++++++++++ 38 files changed, 3612 insertions(+), 71 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusModeEnum.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{modbustcp/codec => modbus/common}/IotModbusDataConverter.java (99%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{modbustcp/IotModbusTcpConfig.java => modbus/tcpmaster/IotModbusTcpMasterConfig.java} (71%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{modbustcp/IotModbusTcpProtocol.java => modbus/tcpmaster/IotModbusTcpMasterProtocol.java} (71%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{modbustcp => modbus/tcpmaster}/client/IotModbusTcpClient.java (97%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{modbustcp => modbus/tcpmaster}/handler/downstream/IotModbusTcpDownstreamHandler.java (90%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{modbustcp => modbus/tcpmaster}/handler/downstream/IotModbusTcpDownstreamSubscriber.java (70%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{modbustcp => modbus/tcpmaster}/handler/upstream/IotModbusTcpUpstreamHandler.java (93%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{modbustcp => modbus/tcpmaster}/manager/IotModbusTcpConfigCacheService.java (97%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{modbustcp => modbus/tcpmaster}/manager/IotModbusTcpConnectionManager.java (98%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{modbustcp => modbus/tcpmaster}/manager/IotModbusTcpPollScheduler.java (93%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{modbustcp => modbus/tcpmaster}/package-info.java (69%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusRecordParserFactory.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusResponseParser.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/ModbusRtuOverTcpDemo.java rename yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/{modbustcp => modbus/tcpmaster}/ModbusTcpSlaveSimulatorTest.java (98%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java index 304b37ff16..60b132c3d1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java @@ -33,6 +33,14 @@ public class IotDeviceModbusConfigRespVO { @Schema(description = "重试间隔(毫秒)", example = "1000") private Integer retryInterval; + // TODO @AI:不要【:1-云端轮询 2-主动上报】 + @Schema(description = "模式:1-云端轮询 2-主动上报", example = "1") + private Integer mode; + + // TODO @AI:还是换成 int,然后写注释;不要【:modbus_tcp / modbus_rtu】 + @Schema(description = "数据帧格式:modbus_tcp / modbus_rtu", example = "modbus_tcp") + private String frameFormat; + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") private Integer status; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java index 01f92b85fd..ae47afcd89 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java @@ -31,6 +31,14 @@ public class IotDeviceModbusConfigSaveReqVO { @Schema(description = "重试间隔(毫秒)", example = "1000") private Integer retryInterval; + // TODO @AI:不要【:1-云端轮询 2-主动上报】 + @Schema(description = "模式:1-云端轮询 2-主动上报", example = "1") + private Integer mode; + + // TODO @AI:不要【:1-云端轮询 2-主动上报】 + @Schema(description = "数据帧格式:modbus_tcp / modbus_rtu", example = "modbus_tcp") + private String frameFormat; + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") @NotNull(message = "状态不能为空") private Integer status; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java index 627ec1b336..06e94d2506 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -24,7 +25,12 @@ public class IotDeviceModbusConfigDO extends TenantBaseDO { */ @TableId private Long id; - // TODO @AI:增加 productId; + /** + * 产品编号 + * + * 关联 {@link IotProductDO#getId()} + */ + private Long productId; /** * 设备编号 * @@ -52,6 +58,18 @@ public class IotDeviceModbusConfigDO extends TenantBaseDO { * 重试间隔,单位:毫秒 */ private Integer retryInterval; + /** + * 模式 + * + * @see cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum + */ + private Integer mode; + /** + * 数据帧格式 + * + * @see cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum + */ + private String frameFormat; /** * 状态 * diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java index 0b382a3ee1..4580a8e596 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java @@ -47,8 +47,16 @@ public class IotModbusDeviceConfigRespDTO { * 重试间隔,单位:毫秒 */ private Integer retryInterval; + /** + * 模式 + */ + private Integer mode; + /** + * 数据帧格式 + */ + private String frameFormat; - // ========== 点位配置 ========== + // ========== Modbus 点位配置 ========== /** * 点位列表 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java new file mode 100644 index 0000000000..b2817047a1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.iot.core.enums; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT Modbus 数据帧格式枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum IotModbusFrameFormatEnum implements ArrayValuable { + + MODBUS_TCP("modbus_tcp", "Modbus TCP"), + MODBUS_RTU("modbus_rtu", "Modbus RTU"); + + public static final String[] ARRAYS = Arrays.stream(values()) + .map(IotModbusFrameFormatEnum::getFormat) + .toArray(String[]::new); + + /** + * 格式 + */ + private final String format; + /** + * 名称 + */ + private final String name; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusModeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusModeEnum.java new file mode 100644 index 0000000000..e8bcf2e68c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusModeEnum.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.iot.core.enums; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT Modbus 模式枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum IotModbusModeEnum implements ArrayValuable { + + POLLING(1, "云端轮询"), + ACTIVE_REPORT(2, "主动上报"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(IotModbusModeEnum::getMode) + .toArray(Integer[]::new); + + /** + * 模式 + */ + private final Integer mode; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java index 5fbd713a8d..e299364755 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java +++ b/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 { MQTT("mqtt"), EMQX("emqx"), COAP("coap"), - MODBUS_TCP("modbus_tcp"); + MODBUS_TCP_MASTER("modbus_tcp_master"), + MODBUS_TCP_SLAVE("modbus_tcp_slave"); public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 8636bbd061..8fcc3eae39 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -4,7 +4,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; @@ -168,10 +169,16 @@ public class IotGatewayProperties { private IotEmqxConfig emqx; /** - * Modbus TCP 协议配置 + * Modbus TCP Master 协议配置 */ @Valid - private IotModbusTcpConfig modbusTcp; + private IotModbusTcpMasterConfig modbusTcpMaster; + + /** + * Modbus TCP Slave 协议配置 + */ + @Valid + private IotModbusTcpSlaveConfig modbusTcpSlave; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index 1549d7d23d..eb9fd06cf1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -7,7 +7,8 @@ import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; @@ -113,8 +114,10 @@ public class IotProtocolManager implements SmartLifecycle { return createMqttProtocol(config); case EMQX: return createEmqxProtocol(config); - case MODBUS_TCP: - return createModbusTcpProtocol(config); + case MODBUS_TCP_MASTER: + return createModbusTcpMasterProtocol(config); + case MODBUS_TCP_SLAVE: + return createModbusTcpSlaveProtocol(config); default: throw new IllegalArgumentException(String.format( "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); @@ -192,13 +195,23 @@ public class IotProtocolManager implements SmartLifecycle { } /** - * 创建 Modbus TCP 协议实例 + * 创建 Modbus TCP Master 协议实例 * * @param config 协议实例配置 - * @return Modbus TCP 协议实例 + * @return Modbus TCP Master 协议实例 */ - private IotModbusTcpProtocol createModbusTcpProtocol(IotGatewayProperties.ProtocolProperties config) { - return new IotModbusTcpProtocol(config); + private IotModbusTcpMasterProtocol createModbusTcpMasterProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotModbusTcpMasterProtocol(config); + } + + /** + * 创建 Modbus TCP Slave 协议实例 + * + * @param config 协议实例配置 + * @return Modbus TCP Slave 协议实例 + */ + private IotModbusTcpSlaveProtocol createModbusTcpSlaveProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotModbusTcpSlaveProtocol(config); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/codec/IotModbusDataConverter.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusDataConverter.java similarity index 99% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/codec/IotModbusDataConverter.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusDataConverter.java index 61a4201ff0..219f20a336 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/codec/IotModbusDataConverter.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusDataConverter.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterConfig.java similarity index 71% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfig.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterConfig.java index 5893869e27..9b4867c360 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterConfig.java @@ -1,16 +1,16 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.Data; /** - * IoT Modbus TCP 协议配置 + * IoT Modbus TCP Master 协议配置 * * @author 芋道源码 */ @Data -public class IotModbusTcpConfig { +public class IotModbusTcpMasterConfig { /** * 配置刷新间隔(秒) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java similarity index 71% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpProtocol.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java index 163e1e5fb7..0d5e86edc0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; import cn.hutool.core.lang.Assert; import cn.hutool.extra.spring.SpringUtil; @@ -9,14 +9,14 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream.IotModbusTcpDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream.IotModbusTcpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.upstream.IotModbusTcpUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpPollScheduler; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; import lombok.Getter; @@ -27,12 +27,12 @@ import java.util.List; import java.util.concurrent.TimeUnit; /** - * IoT 网关 Modbus TCP 协议:主动轮询 Modbus 从站设备数据 + * IoT 网关 Modbus TCP Master 协议:主动轮询 Modbus 从站设备数据 * * @author 芋道源码 */ @Slf4j -public class IotModbusTcpProtocol implements IotProtocol { +public class IotModbusTcpMasterProtocol implements IotProtocol { /** * 协议配置 @@ -71,9 +71,9 @@ public class IotModbusTcpProtocol implements IotProtocol { private final IotModbusTcpConfigCacheService configCacheService; private final IotModbusTcpPollScheduler pollScheduler; - public IotModbusTcpProtocol(ProtocolProperties properties) { - IotModbusTcpConfig modbusTcpConfig = properties.getModbusTcp(); - Assert.notNull(modbusTcpConfig, "Modbus TCP 协议配置(modbusTcp)不能为空"); + public IotModbusTcpMasterProtocol(ProtocolProperties properties) { + IotModbusTcpMasterConfig modbusTcpMasterConfig = properties.getModbusTcpMaster(); + Assert.notNull(modbusTcpMasterConfig, "Modbus TCP Master 协议配置(modbusTcpMaster)不能为空"); this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); @@ -109,13 +109,13 @@ public class IotModbusTcpProtocol implements IotProtocol { @Override public IotProtocolTypeEnum getType() { - return IotProtocolTypeEnum.MODBUS_TCP; + return IotProtocolTypeEnum.MODBUS_TCP_MASTER; } @Override public void start() { if (running) { - log.warn("[start][IoT Modbus TCP 协议 {} 已经在运行中]", getId()); + log.warn("[start][IoT Modbus TCP Master 协议 {} 已经在运行中]", getId()); return; } @@ -123,18 +123,18 @@ public class IotModbusTcpProtocol implements IotProtocol { // 1.1 首次加载配置 refreshConfig(); // 1.2 启动配置刷新定时器 - int refreshInterval = properties.getModbusTcp().getConfigRefreshInterval(); + int refreshInterval = properties.getModbusTcpMaster().getConfigRefreshInterval(); configRefreshTimerId = vertx.setPeriodic( TimeUnit.SECONDS.toMillis(refreshInterval), id -> refreshConfig() ); running = true; - log.info("[start][IoT Modbus TCP 协议 {} 启动成功,serverId={}]", getId(), serverId); + log.info("[start][IoT Modbus TCP Master 协议 {} 启动成功,serverId={}]", getId(), serverId); // 2. 启动下行消息订阅者 this.downstreamSubscriber.start(); } catch (Exception e) { - log.error("[start][IoT Modbus TCP 协议 {} 启动失败]", getId(), e); + log.error("[start][IoT Modbus TCP Master 协议 {} 启动失败]", getId(), e); // 启动失败时关闭资源 if (vertx != null) { vertx.close(); @@ -151,9 +151,9 @@ public class IotModbusTcpProtocol implements IotProtocol { // 1. 停止下行消息订阅者 try { downstreamSubscriber.stop(); - log.info("[stop][IoT Modbus TCP 协议 {} 下行消息订阅者已停止]", getId()); + log.info("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者已停止]", getId()); } catch (Exception e) { - log.error("[stop][IoT Modbus TCP 协议 {} 下行消息订阅者停止失败]", getId(), e); + log.error("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者停止失败]", getId(), e); } // 2.1 取消配置刷新定时器 @@ -163,22 +163,22 @@ public class IotModbusTcpProtocol implements IotProtocol { } // 2.2 停止轮询调度器 pollScheduler.stopAll(); - log.info("[stop][IoT Modbus TCP 协议 {} 轮询调度器已停止]", getId()); + log.info("[stop][IoT Modbus TCP Master 协议 {} 轮询调度器已停止]", getId()); // 2.3 关闭所有连接 connectionManager.closeAll(); - log.info("[stop][IoT Modbus TCP 协议 {} 连接管理器已关闭]", getId()); + log.info("[stop][IoT Modbus TCP Master 协议 {} 连接管理器已关闭]", getId()); // 3. 关闭 Vert.x 实例 if (vertx != null) { try { vertx.close().result(); - log.info("[stop][IoT Modbus TCP 协议 {} Vertx 已关闭]", getId()); + log.info("[stop][IoT Modbus TCP Master 协议 {} Vertx 已关闭]", getId()); } catch (Exception e) { - log.error("[stop][IoT Modbus TCP 协议 {} Vertx 关闭失败]", getId(), e); + log.error("[stop][IoT Modbus TCP Master 协议 {} Vertx 关闭失败]", getId(), e); } } running = false; - log.info("[stop][IoT Modbus TCP 协议 {} 已停止]", getId()); + log.info("[stop][IoT Modbus TCP Master 协议 {} 已停止]", getId()); } /** diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/client/IotModbusTcpClient.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java similarity index 97% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/client/IotModbusTcpClient.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java index 951798a7c2..f789c2c0cd 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/client/IotModbusTcpClient.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java @@ -1,8 +1,8 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; import com.ghgande.j2mod.modbus.msg.*; import com.ghgande.j2mod.modbus.procimg.InputRegister; @@ -12,6 +12,7 @@ import com.ghgande.j2mod.modbus.util.BitVector; import io.vertx.core.Future; import lombok.extern.slf4j.Slf4j; +// TODO @AI:感觉它更像一个工具类;但是名字叫 client 很奇怪; /** * IoT Modbus TCP 客户端 *

                              diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java similarity index 90% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java index 49c3931fed..c430fcbe95 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; @@ -6,10 +6,10 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java similarity index 70% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java index bea82919ef..d50608a0c8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java @@ -1,13 +1,14 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +// TODO @AI:是不是可以继承 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java /** * IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 * @@ -17,7 +18,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber { - private final IotModbusTcpProtocol protocol; + private final IotModbusTcpMasterProtocol protocol; private final IotModbusTcpDownstreamHandler downstreamHandler; private final IotMessageBus messageBus; @@ -26,7 +27,7 @@ public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber * 提供基于 j2mod 的 Modbus TCP 主站(Master)功能,支持: * 1. 定时轮询 Modbus 从站设备数据 * 2. 下发属性设置命令到从站设备 * 3. 数据格式转换(寄存器值 ↔ 物模型属性值) */ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; \ No newline at end of file +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java new file mode 100644 index 0000000000..25d377d2b8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT Modbus TCP Slave 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotModbusTcpSlaveConfig { + + /** + * 配置刷新间隔(秒) + */ + @NotNull(message = "配置刷新间隔不能为空") + @Min(value = 1, message = "配置刷新间隔不能小于 1 秒") + private Integer configRefreshInterval = 30; + + /** + * 自定义功能码(用于认证等扩展交互) + * Modbus 协议保留 65-72 给用户自定义,默认 65 + */ + @NotNull(message = "自定义功能码不能为空") + @Min(value = 65, message = "自定义功能码不能小于 65") + // TODO @AI:搞个范围; + private Integer customFunctionCode = 65; + + /** + * Pending Request 超时时间(毫秒) + */ + @NotNull(message = "请求超时时间不能为空") + private Integer requestTimeout = 5000; + + /** + * Pending Request 清理间隔(毫秒) + */ + @NotNull(message = "请求清理间隔不能为空") + private Integer requestCleanupInterval = 10000; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java new file mode 100644 index 0000000000..a80471b9e8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java @@ -0,0 +1,372 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusRecordParserFactory; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream.IotModbusTcpSlaveUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +import io.vertx.core.net.NetServer; +import io.vertx.core.net.NetServerOptions; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +// TODO @AI:不用主动上报! +/** + * IoT 网关 Modbus TCP Slave 协议 + *

                              + * 作为 TCP Server 接收设备主动连接: + * 1. 设备通过自定义功能码(FC 65)发送认证请求 + * 2. 认证成功后,根据设备配置的 mode 决定工作模式: + * - mode=1(云端轮询):网关主动发送 Modbus 读请求,设备响应 + * - mode=2(主动上报):设备主动上报数据,网关透传 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpSlaveProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private final Vertx vertx; + /** + * TCP Server + */ + private NetServer netServer; + /** + * 配置刷新定时器 ID + */ + private Long configRefreshTimerId; + /** + * Pending Request 清理定时器 ID + */ + private Long requestCleanupTimerId; + + /** + * 未认证连接的帧格式缓存:socket → 检测到的帧格式 + */ + private final Map pendingFrameFormats = new ConcurrentHashMap<>(); + + // ========== 各组件 ========== + + private final IotModbusTcpSlaveConfig slaveConfig; + private final IotModbusFrameCodec frameCodec; + private final IotModbusTcpSlaveConnectionManager connectionManager; + private final IotModbusTcpSlaveConfigCacheService configCacheService; + private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; + private final IotModbusTcpSlaveUpstreamHandler upstreamHandler; + private final IotModbusTcpSlaveDownstreamHandler downstreamHandler; + private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; + private final IotModbusTcpSlavePollScheduler pollScheduler; + + public IotModbusTcpSlaveProtocol(ProtocolProperties properties) { + this.slaveConfig = properties.getModbusTcpSlave(); + Assert.notNull(slaveConfig, "Modbus TCP Slave 协议配置(modbusTcpSlave)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化 Vertx + this.vertx = Vertx.vertx(); + + // 初始化 Manager + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.connectionManager = new IotModbusTcpSlaveConnectionManager(); + this.configCacheService = new IotModbusTcpSlaveConfigCacheService(deviceApi); + this.pendingRequestManager = new IotModbusTcpSlavePendingRequestManager(); + + // 初始化帧编解码器 + this.frameCodec = new IotModbusFrameCodec(slaveConfig.getCustomFunctionCode()); + + // 初始化 Handler + IotModbusDataConverter dataConverter = new IotModbusDataConverter(); + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler( + deviceApi, messageService, dataConverter, frameCodec, + connectionManager, configCacheService, pendingRequestManager, serverId); + this.downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( + connectionManager, configCacheService, dataConverter, frameCodec); + + // 初始化轮询调度器 + this.pollScheduler = new IotModbusTcpSlavePollScheduler( + vertx, connectionManager, frameCodec, pendingRequestManager, + slaveConfig.getRequestTimeout()); + + // 设置认证成功回调:启动轮询 + // TODO @AI:感觉直接去调用,不用注册回调了(更简洁) + this.upstreamHandler.setOnAuthSuccess((deviceId, config) -> { + if (config.getMode() != null + && config.getMode().equals(IotModbusModeEnum.POLLING.getMode())) { + pollScheduler.updatePolling(config); + } + }); + + // 初始化下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber( + this, downstreamHandler, messageBus); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.MODBUS_TCP_SLAVE; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT Modbus TCP Slave 协议 {} 已经在运行中]", getId()); + return; + } + + try { + // 1.1 首次加载配置 + refreshConfig(); + // 1.2 启动配置刷新定时器 + int refreshInterval = slaveConfig.getConfigRefreshInterval(); + configRefreshTimerId = vertx.setPeriodic( + TimeUnit.SECONDS.toMillis(refreshInterval), + id -> refreshConfig()); + + // 2.1 启动 TCP Server + startTcpServer(); + + // 2.2 启动 PendingRequest 清理定时器 + requestCleanupTimerId = vertx.setPeriodic( + slaveConfig.getRequestCleanupInterval(), + id -> pendingRequestManager.cleanupExpired()); + running = true; + log.info("[start][IoT Modbus TCP Slave 协议 {} 启动成功, serverId={}, port={}]", + getId(), serverId, properties.getPort()); + + // 3. 启动下行消息订阅 + downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT Modbus TCP Slave 协议 {} 启动失败]", getId(), e); + if (vertx != null) { + vertx.close(); + } + // TODO @AI:其它相关的 close; + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + + // 1. 停止下行消息订阅 + try { + downstreamSubscriber.stop(); + } catch (Exception e) { + log.error("[stop][下行消息订阅器停止失败]", e); + } + + // 2.1 取消定时器 + if (configRefreshTimerId != null) { + vertx.cancelTimer(configRefreshTimerId); + configRefreshTimerId = null; + } + if (requestCleanupTimerId != null) { + vertx.cancelTimer(requestCleanupTimerId); + requestCleanupTimerId = null; + } + // 2.2 停止轮询 + pollScheduler.stopAll(); + // 2.3 清理 PendingRequest + pendingRequestManager.clear(); + // 2.3 关闭所有连接 + connectionManager.closeAll(); + // 2.4 关闭 TCP Server + if (netServer != null) { + try { + netServer.close().result(); + log.info("[stop][TCP Server 已关闭]"); + } catch (Exception e) { + log.error("[stop][TCP Server 关闭失败]", e); + } + } + + // 3. 关闭 Vertx + if (vertx != null) { + try { + vertx.close().result(); + } catch (Exception e) { + log.error("[stop][Vertx 关闭失败]", e); + } + } + running = false; + log.info("[stop][IoT Modbus TCP Slave 协议 {} 已停止]", getId()); + } + + /** + * 启动 TCP Server + */ + private void startTcpServer() { + // TODO @AI:host 一定要设置么? + // 1. 创建 TCP Server + NetServerOptions options = new NetServerOptions() + .setPort(properties.getPort()) + .setHost("0.0.0.0"); + netServer = vertx.createNetServer(options); + + // 2. 设置连接处理器 + netServer.connectHandler(this::handleConnection); + // TODO @AI:是不是 sync 就好,不用 onSuccess/onFailure 了?感觉更简洁。失败,肯定就要抛出异常,结束初始化了! + netServer.listen() + .onSuccess(server -> log.info("[startTcpServer][TCP Server 启动成功, port={}]", + server.actualPort())) + .onFailure(e -> log.error("[startTcpServer][TCP Server 启动失败]", e)); + } + + /** + * 处理新连接 + */ + private void handleConnection(NetSocket socket) { + log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress()); + + // 1.1 创建带帧格式检测的 RecordParser + // TODO @AI:看看怎么从这个类里面,拿出去;让这个类的职责更单一; + RecordParser parser = IotModbusRecordParserFactory.create( + slaveConfig.getCustomFunctionCode(), + // 完整帧回调 + // TODO @AI:感觉搞个独立的类,稍微好点?! + frameBuffer -> { + byte[] frameBytes = frameBuffer.getBytes(); + // 获取该连接的帧格式 + ConnectionInfo connInfo = connectionManager.getConnectionInfo(socket); + IotModbusFrameFormatEnum frameFormat = connInfo != null ? connInfo.getFrameFormat() : null; + if (frameFormat == null) { + // 未认证的连接,使用首帧检测到的帧格式 + frameFormat = pendingFrameFormats.get(socket); + } + if (frameFormat == null) { + log.warn("[handleConnection][帧格式未检测到, remoteAddress={}]", socket.remoteAddress()); + return; + } + + // 解码帧 + IotModbusFrame frame = frameCodec.decodeResponse(frameBytes, frameFormat); + // 交给 UpstreamHandler 处理 + upstreamHandler.handleFrame(socket, frame, frameFormat); + }, + // 帧格式检测回调:保存到未认证缓存 + detectedFormat -> { + // TODO @AI:是不是不用缓存,每次都探测;因为一般 auth 首包后,基本也没探测的诉求了! + pendingFrameFormats.put(socket, detectedFormat); + // 如果连接已注册(不太可能在检测阶段),也更新 + // TODO @AI:是否非必须?! + connectionManager.setFrameFormat(socket, detectedFormat); + log.debug("[handleConnection][帧格式检测: {}, remoteAddress={}]", + detectedFormat, socket.remoteAddress()); + } + ); + // 1.2 设置数据处理器 + socket.handler(parser); + + // 2.1 连接关闭处理 + socket.closeHandler(v -> { + pendingFrameFormats.remove(socket); + ConnectionInfo info = connectionManager.removeConnection(socket); + // TODO @AI:if return 简化下; + if (info != null && info.getDeviceId() != null) { + pollScheduler.stopPolling(info.getDeviceId()); + pendingRequestManager.removeDevice(info.getDeviceId()); + log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", + info.getDeviceId(), socket.remoteAddress()); + } else { + log.info("[handleConnection][未认证连接关闭, remoteAddress={}]", socket.remoteAddress()); + } + }); + // 2.2 异常处理 + socket.exceptionHandler(e -> { + log.error("[handleConnection][连接异常, remoteAddress={}]", socket.remoteAddress(), e); + socket.close(); + }); + } + + /** + * 刷新配置 + */ + private synchronized void refreshConfig() { + try { + // 1. 从 biz 拉取最新配置 + List configs = configCacheService.refreshConfig(); + log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); + + // 2. 更新已连接设备的轮询任务(仅 mode=1) + for (IotModbusDeviceConfigRespDTO config : configs) { + try { + if (config.getMode() != null + && config.getMode().equals(IotModbusModeEnum.POLLING.getMode())) { + // 只有已连接的设备才启动轮询 + ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(config.getDeviceId()); + if (connInfo != null) { + pollScheduler.updatePolling(config); + } + } + } catch (Exception e) { + log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); + } + } + + // 3. 清理已删除设备的资源 + configCacheService.cleanupRemovedDevices(configs, deviceId -> { + pollScheduler.stopPolling(deviceId); + pendingRequestManager.removeDevice(deviceId); + }); + } catch (Exception e) { + log.error("[refreshConfig][刷新配置失败]", e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java new file mode 100644 index 0000000000..a13c8148f2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * IoT Modbus 统一帧数据模型(TCP/RTU 公用) + * + * @author 芋道源码 + */ +@Data +@Accessors(chain = true) +public class IotModbusFrame { + + /** + * 从站地址 + */ + private int slaveId; + /** + * 功能码 + */ + private int functionCode; + /** + * PDU 数据(不含 slaveId) + */ + private byte[] pdu; + /** + * 事务标识符(TCP 模式特有) + * + * // TODO @AI:最好是 @某个类型独有; + */ + private Integer transactionId; + + /** + * 是否异常响应 + */ + private boolean exception; + // TODO @AI:是不是要枚举一些异常;另外,是不是覆盖掉 exception;因为只要判断有异常码是不是就可以了; + /** + * 异常码(当 exception=true 时有效) + */ + private Integer exceptionCode; + + /** + * 自定义功能码时的 JSON 字符串 + */ + private String customData; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameCodec.java new file mode 100644 index 0000000000..8d65b30bc1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameCodec.java @@ -0,0 +1,317 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; + +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import lombok.extern.slf4j.Slf4j; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * IoT Modbus 帧编解码器 + *

                              + * 纯 Modbus 协议编解码,不处理 TCP 粘包(由 RecordParser 处理)。 + * 支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式,以及自定义功能码扩展。 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusFrameCodec { + + private final int customFunctionCode; + + public IotModbusFrameCodec(int customFunctionCode) { + this.customFunctionCode = customFunctionCode; + } + + // ==================== 解码 ==================== + + /** + * 解码响应帧(拆包后的完整帧 byte[]) + * + * @param data 完整帧字节数组 + * @param format 帧格式 + * @return 解码后的 IotModbusFrame + */ + public IotModbusFrame decodeResponse(byte[] data, IotModbusFrameFormatEnum format) { + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { + return decodeTcpResponse(data); + } else { + return decodeRtuResponse(data); + } + } + + /** + * 解码 MODBUS_TCP 响应 + * 格式:[TransactionId(2)] [ProtocolId(2)] [Length(2)] [UnitId(1)] [FC(1)] [Data...] + */ + private IotModbusFrame decodeTcpResponse(byte[] data) { + if (data.length < 8) { + log.warn("[decodeTcpResponse][数据长度不足: {}]", data.length); + return null; + } + ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN); + int transactionId = buf.getShort() & 0xFFFF; + buf.getShort(); // protocolId(跳过)// TODO @AI:跳过原因,最好写下; + buf.getShort(); // length(跳过)// TODO @AI:跳过原因,最好写下; + int slaveId = buf.get() & 0xFF; + int functionCode = buf.get() & 0xFF; + // 提取 PDU 数据(从 functionCode 之后到末尾) + byte[] pdu = new byte[data.length - 8]; + System.arraycopy(data, 8, pdu, 0, pdu.length); + + // 构建 IotModbusFrame + return buildFrame(slaveId, functionCode, pdu, transactionId); + } + + /** + * 解码 MODBUS_RTU 响应 + * 格式:[SlaveId(1)] [FC(1)] [Data...] [CRC(2)] + */ + private IotModbusFrame decodeRtuResponse(byte[] data) { + if (data.length < 4) { + log.warn("[decodeRtuResponse][数据长度不足: {}]", data.length); + return null; + } + // 校验 CRC + if (!verifyCrc16(data)) { + log.warn("[decodeRtuResponse][CRC 校验失败]"); + return null; + } + int slaveId = data[0] & 0xFF; + int functionCode = data[1] & 0xFF; + // PDU 数据(不含 slaveId、functionCode、CRC) + byte[] pdu = new byte[data.length - 4]; + System.arraycopy(data, 2, pdu, 0, pdu.length); + + // 构建 IotModbusFrame + return buildFrame(slaveId, functionCode, pdu, null); + } + + /** + * 构建 IotModbusFrame + */ + private IotModbusFrame buildFrame(int slaveId, int functionCode, byte[] pdu, Integer transactionId) { + IotModbusFrame frame = new IotModbusFrame() + .setSlaveId(slaveId) + .setFunctionCode(functionCode) + .setPdu(pdu) + .setTransactionId(transactionId); + + // 异常响应 + // TODO @AI:0x80 看看是不是要枚举; + if ((functionCode & 0x80) != 0) { + frame.setException(true); + // TODO @AI:0x7f 看看是不是要枚举; + frame.setFunctionCode(functionCode & 0x7F); + if (pdu.length >= 1) { + frame.setExceptionCode(pdu[0] & 0xFF); + } + return frame; + } + + // 自定义功能码 + if (functionCode == customFunctionCode) { + // data 区格式:[byteCount(1)] [JSON data(N)] + if (pdu.length >= 1) { + int byteCount = pdu[0] & 0xFF; + if (pdu.length >= 1 + byteCount) { + frame.setCustomData(new String(pdu, 1, byteCount, StandardCharsets.UTF_8)); + } + } + } + return frame; + } + + // ==================== 编码 ==================== + + /** + * 编码读请求 + * + * @param slaveId 从站地址 + * @param functionCode 功能码 + * @param startAddress 起始寄存器地址 + * @param quantity 寄存器数量 + * @param format 帧格式 + * @param transactionId 事务 ID(TCP 模式下使用) + * @return 编码后的字节数组 + */ + public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity, + IotModbusFrameFormatEnum format, int transactionId) { + // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)] + byte[] pdu = new byte[5]; + pdu[0] = (byte) functionCode; + pdu[1] = (byte) ((startAddress >> 8) & 0xFF); + pdu[2] = (byte) (startAddress & 0xFF); + pdu[3] = (byte) ((quantity >> 8) & 0xFF); + pdu[4] = (byte) (quantity & 0xFF); + return wrapFrame(slaveId, pdu, format, transactionId); + } + + /** + * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05) + * + * @param slaveId 从站地址 + * @param functionCode 功能码 + * @param address 寄存器地址 + * @param value 值 + * @param format 帧格式 + * @param transactionId 事务 ID + * @return 编码后的字节数组 + */ + public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value, + IotModbusFrameFormatEnum format, int transactionId) { + // PDU: [FC(1)] [Address(2)] [Value(2)] + byte[] pdu = new byte[5]; + pdu[0] = (byte) functionCode; + pdu[1] = (byte) ((address >> 8) & 0xFF); + pdu[2] = (byte) (address & 0xFF); + pdu[3] = (byte) ((value >> 8) & 0xFF); + pdu[4] = (byte) (value & 0xFF); + return wrapFrame(slaveId, pdu, format, transactionId); + } + + /** + * 编码写多个寄存器请求(FC16) + * + * @param slaveId 从站地址 + * @param address 起始地址 + * @param values 值数组 + * @param format 帧格式 + * @param transactionId 事务 ID + * @return 编码后的字节数组 + */ + public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values, + IotModbusFrameFormatEnum format, int transactionId) { + // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)] + int quantity = values.length; + int byteCount = quantity * 2; + byte[] pdu = new byte[6 + byteCount]; + pdu[0] = (byte) 16; // FC16 + pdu[1] = (byte) ((address >> 8) & 0xFF); + pdu[2] = (byte) (address & 0xFF); + pdu[3] = (byte) ((quantity >> 8) & 0xFF); + pdu[4] = (byte) (quantity & 0xFF); + pdu[5] = (byte) byteCount; + for (int i = 0; i < quantity; i++) { + pdu[6 + i * 2] = (byte) ((values[i] >> 8) & 0xFF); + pdu[6 + i * 2 + 1] = (byte) (values[i] & 0xFF); + } + return wrapFrame(slaveId, pdu, format, transactionId); + } + + /** + * 编码自定义功能码帧(认证响应等) + * + * @param slaveId 从站地址 + * @param jsonData JSON 数据 + * @param format 帧格式 + * @param transactionId 事务 ID + * @return 编码后的字节数组 + */ + public byte[] encodeCustomFrame(int slaveId, String jsonData, + IotModbusFrameFormatEnum format, int transactionId) { + byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8); + // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)] + byte[] pdu = new byte[2 + jsonBytes.length]; + pdu[0] = (byte) customFunctionCode; + pdu[1] = (byte) jsonBytes.length; + System.arraycopy(jsonBytes, 0, pdu, 2, jsonBytes.length); + return wrapFrame(slaveId, pdu, format, transactionId); + } + + // ==================== 帧封装 ==================== + + /** + * 将 PDU 封装为完整帧 + * + * @param slaveId 从站地址 + * @param pdu PDU 数据(含 functionCode) + * @param format 帧格式 + * @param transactionId 事务 ID(TCP 模式下使用) + * @return 完整帧字节数组 + */ + private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, int transactionId) { + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { + return wrapTcpFrame(slaveId, pdu, transactionId); + } else { + return wrapRtuFrame(slaveId, pdu); + } + } + + /** + * 封装 MODBUS_TCP 帧 + * [TransactionId(2)] [ProtocolId(2,=0x0000)] [Length(2)] [UnitId(1)] [PDU...] + */ + private byte[] wrapTcpFrame(int slaveId, byte[] pdu, int transactionId) { + int length = 1 + pdu.length; // UnitId + PDU + byte[] frame = new byte[6 + length]; // MBAP(6) + UnitId(1) + PDU + // MBAP Header + frame[0] = (byte) ((transactionId >> 8) & 0xFF); + frame[1] = (byte) (transactionId & 0xFF); + frame[2] = 0; // Protocol ID high + frame[3] = 0; // Protocol ID low + frame[4] = (byte) ((length >> 8) & 0xFF); + frame[5] = (byte) (length & 0xFF); + // Unit ID + frame[6] = (byte) slaveId; + // PDU + System.arraycopy(pdu, 0, frame, 7, pdu.length); + return frame; + } + + /** + * 封装 MODBUS_RTU 帧 + * [SlaveId(1)] [PDU...] [CRC(2)] + */ + private byte[] wrapRtuFrame(int slaveId, byte[] pdu) { + byte[] frame = new byte[1 + pdu.length + 2]; // SlaveId + PDU + CRC + frame[0] = (byte) slaveId; + System.arraycopy(pdu, 0, frame, 1, pdu.length); + // 计算并追加 CRC16 + int crc = calculateCrc16(frame, frame.length - 2); + frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low + frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High + return frame; + } + + // ==================== CRC16 工具 ==================== + + // TODO @AI:hutool 等,有没工具类可以用 + /** + * 计算 CRC-16/MODBUS + * + * @param data 数据 + * @param length 计算长度 + * @return CRC16 值 + */ + public static int calculateCrc16(byte[] data, int length) { + int crc = 0xFFFF; + for (int i = 0; i < length; i++) { + crc ^= (data[i] & 0xFF); + for (int j = 0; j < 8; j++) { + if ((crc & 0x0001) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; + } + + // TODO @AI:hutool 等,有没工具类可以用 + /** + * 校验 CRC16 + */ + private boolean verifyCrc16(byte[] data) { + if (data.length < 3) { + return false; + } + int computed = calculateCrc16(data, data.length - 2); + int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8); + return computed == received; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusRecordParserFactory.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusRecordParserFactory.java new file mode 100644 index 0000000000..07d98d3d55 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusRecordParserFactory.java @@ -0,0 +1,362 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; + +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; + +import java.util.function.Consumer; + +// TODO @AI:看看是不是不要搞成 factory,而是直接 new;(可以一起讨论下) +/** + * IoT Modbus RecordParser 工厂 + *

                              + * 创建带自动帧格式检测的 RecordParser: + * 1. 首帧检测:读前 6 字节,判断 MODBUS_TCP(ProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU + * 2. 检测后自动切换到对应的拆包模式 + * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动) + * - MODBUS_RTU:功能码驱动的状态机 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusRecordParserFactory { + + /** + * 创建带自动帧格式检测的 RecordParser + * + * @param customFunctionCode 自定义功能码 + * @param frameHandler 完整帧回调 + * @param onFormatDetected 帧格式检测回调 + * @return RecordParser 实例 + */ + public static RecordParser create(int customFunctionCode, + Handler frameHandler, + Consumer onFormatDetected) { + // 先创建一个 RecordParser,使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测 + // TODO @AI:最小需要 6 个字节么?有可能更小的情况下,就探测出来?! + RecordParser parser = RecordParser.newFixed(6); + parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler, onFormatDetected)); + return parser; + } + + /** + * 帧格式检测阶段 Handler + */ + @SuppressWarnings("ClassCanBeRecord") + private static class DetectPhaseHandler implements Handler { + + private final RecordParser parser; + private final int customFunctionCode; + private final Handler frameHandler; + private final Consumer onFormatDetected; + + // TODO @AI:简化构造方法,使用 lombok; + DetectPhaseHandler(RecordParser parser, int customFunctionCode, + Handler frameHandler, + Consumer onFormatDetected) { + this.parser = parser; + this.customFunctionCode = customFunctionCode; + this.frameHandler = frameHandler; + this.onFormatDetected = onFormatDetected; + } + + @Override + public void handle(Buffer buffer) { + byte[] header = buffer.getBytes(); + // 检测:byte[2]==0x00 && byte[3]==0x00 && 1<=length<=253 + int protocolId = ((header[2] & 0xFF) << 8) | (header[3] & 0xFF); + int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF); + + if (protocolId == 0x0000 && length >= 1 && length <= 253) { + // MODBUS_TCP + log.debug("[DetectPhaseHandler][检测到 MODBUS_TCP 帧格式]"); + onFormatDetected.accept(IotModbusFrameFormatEnum.MODBUS_TCP); + // 切换到 TCP 拆包模式,处理当前首帧 + TcpFrameHandler tcpHandler = new TcpFrameHandler(parser, frameHandler); + parser.handler(tcpHandler); + // 当前 header 是 MBAP 的前 6 字节,需要继续读 length 字节 + tcpHandler.handleMbapHeader(header, length); + } else { + // MODBUS_RTU + log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]"); + onFormatDetected.accept(IotModbusFrameFormatEnum.MODBUS_RTU); + // 切换到 RTU 拆包模式,处理当前首帧 + RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, customFunctionCode, frameHandler); + parser.handler(rtuHandler); + // 当前 header 包含前 6 字节(slaveId + FC + 部分数据),需要拼接处理 + rtuHandler.handleInitialBytes(header); + } + } + } + + /** + * MODBUS_TCP 拆包 Handler(两阶段 RecordParser) + * Phase 1: fixedSizeMode(6) → 读 MBAP 前 6 字节,提取 length + * Phase 2: fixedSizeMode(length) → 读 unitId + PDU + */ + private static class TcpFrameHandler implements Handler { + + private final RecordParser parser; + private final Handler frameHandler; + private byte[] mbapHeader; + private boolean waitingForBody = false; + + // TODO @AI:lombok + TcpFrameHandler(RecordParser parser, Handler frameHandler) { + this.parser = parser; + this.frameHandler = frameHandler; + } + + /** + * 处理首帧的 MBAP 头 + */ + void handleMbapHeader(byte[] header, int length) { + this.mbapHeader = header; + this.waitingForBody = true; + parser.fixedSizeMode(length); + } + + @Override + public void handle(Buffer buffer) { + if (waitingForBody) { + // Phase 2: 收到 body(unitId + PDU) + byte[] body = buffer.getBytes(); + // 拼接完整帧:MBAP(6) + body + Buffer frame = Buffer.buffer(mbapHeader.length + body.length); + frame.appendBytes(mbapHeader); + frame.appendBytes(body); + frameHandler.handle(frame); + // 切回 Phase 1 + waitingForBody = false; + mbapHeader = null; + parser.fixedSizeMode(6); + } else { + // Phase 1: 收到 MBAP 头 6 字节 + byte[] header = buffer.getBytes(); + int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF); + if (length < 1 || length > 253) { + log.warn("[TcpFrameHandler][MBAP Length 异常: {}]", length); + parser.fixedSizeMode(6); + return; + } + this.mbapHeader = header; + this.waitingForBody = true; + parser.fixedSizeMode(length); + } + } + } + + /** + * MODBUS_RTU 拆包 Handler(功能码驱动的状态机) + *

                              + * 状态机流程: + * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode + * Phase 2: 根据 functionCode 确定剩余长度: + * - 异常响应 (FC & 0x80):fixedSizeMode(3) → exceptionCode(1) + CRC(2) + * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2) + * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2) + * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2) + */ + private static class RtuFrameHandler implements Handler { + + private static final int STATE_HEADER = 0; + private static final int STATE_EXCEPTION_BODY = 1; + private static final int STATE_READ_BYTE_COUNT = 2; + private static final int STATE_READ_DATA = 3; + private static final int STATE_WRITE_BODY = 4; + + private final RecordParser parser; + private final int customFunctionCode; + private final Handler frameHandler; + + private int state = STATE_HEADER; + private byte slaveId; + private byte functionCode; + private byte byteCount; + + // TODO @AI:lombok + RtuFrameHandler(RecordParser parser, int customFunctionCode, Handler frameHandler) { + this.parser = parser; + this.customFunctionCode = customFunctionCode; + this.frameHandler = frameHandler; + } + + /** + * 处理首帧检测阶段传来的初始 6 字节 + * 由于 RTU 首帧跳过了格式检测,我们需要拼接处理 + */ + void handleInitialBytes(byte[] initialBytes) { + // initialBytes 包含 6 字节:[slaveId][FC][...4 bytes...] + this.slaveId = initialBytes[0]; + this.functionCode = initialBytes[1]; + int fc = functionCode & 0xFF; + + // 根据功能码,确定还需要多少字节 + if ((fc & 0x80) != 0) { + // 异常响应:还需要 exceptionCode(1) + CRC(2) = 3 字节 + // 我们已经有 4 字节剩余(initialBytes[2..5]),足够 + // 拼接完整帧并交付 + // 完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 + Buffer frame = Buffer.buffer(5); + frame.appendByte(slaveId); + frame.appendByte(functionCode); + frame.appendBytes(initialBytes, 2, 3); // exceptionCode + CRC + frameHandler.handle(frame); + // 剩余 1 字节需要留给下一帧,但 RecordParser 不支持回推 + // 简化处理:重置状态,开始读下一帧 + resetToHeader(); + } else if (isReadResponse(fc) || fc == customFunctionCode) { + // 读响应或自定义 FC:initialBytes[2] = byteCount + this.byteCount = initialBytes[2]; + int bc = byteCount & 0xFF; + // 已有数据:initialBytes[3..5] = 3 字节 + // 还需:byteCount + CRC(2) - 3 字节已有 + int remaining = bc + 2 - 3; + if (remaining <= 0) { + // 数据已足够,组装完整帧 + int totalLen = 2 + 1 + bc + 2; // slaveId + FC + byteCount + data + CRC + Buffer frame = Buffer.buffer(totalLen); + frame.appendByte(slaveId); + frame.appendByte(functionCode); + frame.appendByte(byteCount); + frame.appendBytes(initialBytes, 3, bc + 2); // data + CRC + frameHandler.handle(frame); + resetToHeader(); + } else { + // 需要继续读 + state = STATE_READ_DATA; + // 保存已有数据片段 + parser.fixedSizeMode(remaining); + // 在 handle() 中需要拼接 initialBytes[3..5] + 新读取的数据 + // 为了简化,我们用一个 Buffer 暂存 + this.pendingData = Buffer.buffer(); + this.pendingData.appendBytes(initialBytes, 3, 3); + this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC + } + } else if (isWriteResponse(fc)) { + // 写响应:FC05/06/15/16,总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 + // 已有 6 字节,还需 2 字节 + state = STATE_WRITE_BODY; + this.pendingData = Buffer.buffer(); + this.pendingData.appendBytes(initialBytes, 2, 4); // 4 bytes already + parser.fixedSizeMode(2); // need 2 more bytes (CRC) + } else { + log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc)); + resetToHeader(); + } + } + + private Buffer pendingData; + private int expectedDataLen; + + @Override + public void handle(Buffer buffer) { + switch (state) { + case STATE_HEADER: + handleHeader(buffer); + break; + case STATE_EXCEPTION_BODY: + handleExceptionBody(buffer); + break; + case STATE_READ_BYTE_COUNT: + handleReadByteCount(buffer); + break; + case STATE_READ_DATA: + handleReadData(buffer); + break; + case STATE_WRITE_BODY: + handleWriteBody(buffer); + break; + default: + resetToHeader(); + } + } + + private void handleHeader(Buffer buffer) { + byte[] header = buffer.getBytes(); + this.slaveId = header[0]; + this.functionCode = header[1]; + int fc = functionCode & 0xFF; + + if ((fc & 0x80) != 0) { + // 异常响应 + state = STATE_EXCEPTION_BODY; + parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2) + } else if (isReadResponse(fc) || fc == customFunctionCode) { + // 读响应或自定义 FC + state = STATE_READ_BYTE_COUNT; + parser.fixedSizeMode(1); // byteCount + } else if (isWriteResponse(fc)) { + // 写响应 + state = STATE_WRITE_BODY; + pendingData = Buffer.buffer(); + parser.fixedSizeMode(6); // addr(2) + value(2) + CRC(2) + } else { + log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc)); + resetToHeader(); + } + } + + private void handleExceptionBody(Buffer buffer) { + // buffer = exceptionCode(1) + CRC(2) + Buffer frame = Buffer.buffer(); + frame.appendByte(slaveId); + frame.appendByte(functionCode); + frame.appendBuffer(buffer); + frameHandler.handle(frame); + resetToHeader(); + } + + private void handleReadByteCount(Buffer buffer) { + this.byteCount = buffer.getByte(0); + int bc = byteCount & 0xFF; + state = STATE_READ_DATA; + pendingData = Buffer.buffer(); + expectedDataLen = bc + 2; // data(bc) + CRC(2) + parser.fixedSizeMode(expectedDataLen); + } + + private void handleReadData(Buffer buffer) { + pendingData.appendBuffer(buffer); + if (pendingData.length() >= expectedDataLen) { + // 组装完整帧 + Buffer frame = Buffer.buffer(); + frame.appendByte(slaveId); + frame.appendByte(functionCode); + frame.appendByte(byteCount); + frame.appendBuffer(pendingData); + frameHandler.handle(frame); + resetToHeader(); + } + // 否则继续等待(不应该发生,因为我们精确设置了 fixedSizeMode) + } + + private void handleWriteBody(Buffer buffer) { + pendingData.appendBuffer(buffer); + // 完整帧 + Buffer frame = Buffer.buffer(); + frame.appendByte(slaveId); + frame.appendByte(functionCode); + frame.appendBuffer(pendingData); + frameHandler.handle(frame); + resetToHeader(); + } + + private void resetToHeader() { + state = STATE_HEADER; + pendingData = null; + parser.fixedSizeMode(2); // slaveId + FC + } + + private boolean isReadResponse(int fc) { + return fc >= 1 && fc <= 4; + } + + private boolean isWriteResponse(int fc) { + return fc == 5 || fc == 6 || fc == 15 || fc == 16; + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusResponseParser.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusResponseParser.java new file mode 100644 index 0000000000..6fa363db0b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusResponseParser.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; + +import lombok.extern.slf4j.Slf4j; + +/** + * IoT Modbus 响应值提取器 + *

                              + * 从解码后的 IotModbusFrame 中提取寄存器值,用于后续的点位翻译。 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusResponseParser { + + /** + * 从帧中提取寄存器值(FC01-04 读响应) + * + * @param frame 解码后的 Modbus 帧 + * @return 寄存器值数组(int[]),失败返回 null + */ + @SuppressWarnings("EnhancedSwitchMigration") + public static int[] extractValues(IotModbusFrame frame) { + if (frame == null || frame.isException()) { + return null; + } + byte[] pdu = frame.getPdu(); + if (pdu == null || pdu.length < 1) { + return null; + } + + // TODO @AI:jmodbus 看看有没可以复用的枚举类 + int functionCode = frame.getFunctionCode(); + switch (functionCode) { + case 1: // Read Coils + case 2: // Read Discrete Inputs + return extractCoilValues(pdu); + case 3: // Read Holding Registers + case 4: // Read Input Registers + return extractRegisterValues(pdu); + default: + log.warn("[extractValues][不支持的功能码: {}]", functionCode); + return null; + } + } + + /** + * 提取线圈/离散输入值 + * PDU 格式(FC01/02 响应):[ByteCount(1)] [CoilStatus(N)] + */ + private static int[] extractCoilValues(byte[] pdu) { + if (pdu.length < 2) { + return null; + } + int byteCount = pdu[0] & 0xFF; + int bitCount = byteCount * 8; + int[] values = new int[bitCount]; + for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) { + values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01); + } + return values; + } + + /** + * 提取寄存器值 + * PDU 格式(FC03/04 响应):[ByteCount(1)] [RegisterData(N*2)] + */ + private static int[] extractRegisterValues(byte[] pdu) { + if (pdu.length < 2) { + return null; + } + int byteCount = pdu[0] & 0xFF; + int registerCount = byteCount / 2; + int[] values = new int[registerCount]; + for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) { + values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF); + } + return values; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java new file mode 100644 index 0000000000..76e904ee7b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java @@ -0,0 +1,152 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +// TODO @AI:看看能不能和 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java 有一些复用逻辑; +/** + * IoT Modbus TCP Slave 下行消息处理器 + *

                              + * 负责: + * 1. 处理下行消息(如属性设置 thing.service.property.set) + * 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpSlaveDownstreamHandler { + + private final IotModbusTcpSlaveConnectionManager connectionManager; + private final IotModbusTcpSlaveConfigCacheService configCacheService; + private final IotModbusDataConverter dataConverter; + private final IotModbusFrameCodec frameCodec; + + /** + * TCP 事务 ID 自增器 + */ + private final AtomicInteger transactionIdCounter = new AtomicInteger(0); + + /** + * 处理下行消息 + */ + @SuppressWarnings("unchecked") + public void handle(IotDeviceMessage message) { + // 1.1 检查是否是属性设置消息 + if (!IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod().equals(message.getMethod())) { + log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); + return; + } + // 1.2 获取设备配置 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId()); + if (config == null) { + log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId()); + return; + } + // 1.3 获取连接信息 + ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId()); + if (connInfo == null) { + log.warn("[handle][设备 {} 没有连接]", message.getDeviceId()); + return; + } + + // 2. 解析属性值并写入 + Object params = message.getParams(); + if (!(params instanceof Map)) { + log.warn("[handle][params 不是 Map 类型: {}]", params); + return; + } + Map propertyMap = (Map) params; + for (Map.Entry entry : propertyMap.entrySet()) { + String identifier = entry.getKey(); + Object value = entry.getValue(); + // 2.1 查找对应的点位配置 + IotModbusPointRespDTO point = findPoint(config, identifier); + if (point == null) { + log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); + continue; + } + // 2.2 检查是否支持写操作 + if (!isWritable(point.getFunctionCode())) { + log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); + continue; + } + + // 2.3 执行写入 + writeProperty(config.getDeviceId(), connInfo, point, value); + } + } + + /** + * 写入属性值 + */ + private void writeProperty(Long deviceId, ConnectionInfo connInfo, + IotModbusPointRespDTO point, Object value) { + // 1. 转换属性值为原始值 + int[] rawValues = dataConverter.convertToRawValues(value, point); + + // 2. 确定帧格式和事务 ID + IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); + if (frameFormat == null) { + frameFormat = IotModbusFrameFormatEnum.MODBUS_TCP; + } + int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; + int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; + + // 3. 编码写请求 + byte[] data; + IotModbusFunctionCodeEnum fcEnum = IotModbusFunctionCodeEnum.valueOf(point.getFunctionCode()); + if (fcEnum == null) { + log.warn("[writeProperty][未知功能码: {}]", point.getFunctionCode()); + return; + } + if (rawValues.length == 1 && fcEnum.getWriteSingleCode() != null) { + // 单个值:使用单写功能码(FC05/FC06) + data = frameCodec.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(), + point.getRegisterAddress(), rawValues[0], frameFormat, transactionId); + } else if (fcEnum.getWriteMultipleCode() != null) { + // 多个值:使用多写功能码(FC15/FC16) + data = frameCodec.encodeWriteMultipleRegistersRequest(slaveId, + point.getRegisterAddress(), rawValues, frameFormat, transactionId); + } else { + log.warn("[writeProperty][点位 {} 不支持写操作]", point.getIdentifier()); + return; + } + + // 4. 发送 + connectionManager.sendToDevice(deviceId, data); + log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", + deviceId, point.getIdentifier(), value); + } + + /** + * 查找点位配置 + */ + private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { + return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); + } + + /** + * 检查功能码是否支持写操作 + */ + private boolean isWritable(Integer functionCode) { + IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); + return functionCodeEnum != null && Boolean.TRUE.equals(functionCodeEnum.getWritable()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java new file mode 100644 index 0000000000..5c74c27a9d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +// TODO @AI:是不是可以继承 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java +/** + * IoT Modbus TCP Slave 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpSlaveDownstreamSubscriber implements IotMessageSubscriber { + + private final IotModbusTcpSlaveProtocol protocol; + private final IotModbusTcpSlaveDownstreamHandler downstreamHandler; + private final IotMessageBus messageBus; + + /** + * 启动订阅 + */ + public void start() { + messageBus.register(this); + log.info("[start][Modbus TCP Slave 下行消息订阅器已启动, topic={}]", getTopic()); + } + + /** + * 停止订阅 + */ + public void stop() { + messageBus.unregister(this); + log.info("[stop][Modbus TCP Slave 下行消息订阅器已停止]"); + } + + @Override + public String getTopic() { + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); + } + + @Override + public String getGroup() { + return getTopic(); // 点对点消费 + } + + @Override + public void onMessage(IotDeviceMessage message) { + log.debug("[onMessage][收到下行消息: {}]", message); + try { + downstreamHandler.handle(message); + } catch (Exception e) { + log.error("[onMessage][处理下行消息失败]", e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java new file mode 100644 index 0000000000..4d91b636bf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java @@ -0,0 +1,312 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusResponseParser; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.net.NetSocket; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.function.BiConsumer; + +// TODO @AI:逻辑有点多,看看是不是分区域! +/** + * IoT Modbus TCP Slave 上行数据处理器 + *

                              + * 处理: + * 1. 自定义 FC 认证 + * 2. 轮询响应(mode=1)→ 点位翻译 → thing.property.post + * 3. 主动上报(mode=2)→ 透传 property.report TODO @AI:这种模式,应该不用支持;因为主动上报,都走标准的 tcp 即可; + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpSlaveUpstreamHandler { + + private final IotDeviceCommonApi deviceApi; + private final IotDeviceMessageService messageService; + private final IotModbusDataConverter dataConverter; + private final IotModbusFrameCodec frameCodec; + private final IotModbusTcpSlaveConnectionManager connectionManager; + private final IotModbusTcpSlaveConfigCacheService configCacheService; + private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; + private final String serverId; + + /** + * 认证成功回调:(deviceId, config) → 启动轮询等 + */ + @Setter + private BiConsumer onAuthSuccess; + + public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi, + IotDeviceMessageService messageService, + IotModbusDataConverter dataConverter, + IotModbusFrameCodec frameCodec, + IotModbusTcpSlaveConnectionManager connectionManager, + IotModbusTcpSlaveConfigCacheService configCacheService, + IotModbusTcpSlavePendingRequestManager pendingRequestManager, + String serverId) { + this.deviceApi = deviceApi; + this.messageService = messageService; + this.dataConverter = dataConverter; + this.frameCodec = frameCodec; + this.connectionManager = connectionManager; + this.configCacheService = configCacheService; + this.pendingRequestManager = pendingRequestManager; + this.serverId = serverId; + } + + /** + * 处理帧 + */ + public void handleFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { + if (frame == null) { + return; + } + // 1.1 自定义功能码(认证等扩展) + if (StrUtil.isNotEmpty(frame.getCustomData())) { + handleCustomFrame(socket, frame, frameFormat); + return; + } + // 1.2 异常响应 + if (frame.isException()) { + // TODO @AI:这种需要返回一个结果给 modbus client? + log.warn("[handleFrame][设备异常响应, slaveId={}, FC={}, exceptionCode={}]", + frame.getSlaveId(), frame.getFunctionCode(), frame.getExceptionCode()); + return; + } + // 1.3 未认证连接,丢弃 + if (!connectionManager.isAuthenticated(socket)) { + // TODO @AI:这种需要返回一个结果给 modbus client? + log.warn("[handleFrame][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress()); + return; + } + + // TODO @AI:获取不到,看看要不要也打个告警;然后 + // 2. 标准 Modbus 响应 + ConnectionInfo info = connectionManager.getConnectionInfo(socket); + if (info == null) { + return; + } + // TODO @AI:可以断言下,必须是云端轮询; + if (info.getMode() != null && info.getMode().equals(IotModbusModeEnum.ACTIVE_REPORT.getMode())) { + // mode=2:主动上报,透传 + handleActiveReport(info, frame); + } else { + // mode=1:云端轮询,匹配 PendingRequest + handlePollingResponse(info, frame, frameFormat); + } + } + + /** + * 处理自定义功能码帧 + */ + private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { + try { + // TODO @AI:直接使用 JsonUtils 去解析出 IotDeviceMessage + JSONObject json = JSONUtil.parseObj(frame.getCustomData()); + String method = json.getStr("method"); + // TODO @AI: method 枚举下; + if ("auth".equals(method)) { + handleAuth(socket, frame, json, frameFormat); + return; + } + // TODO @AI:把 frame 都打印下; + log.warn("[handleCustomFrame][未知 method: {}]", method); + } catch (Exception e) { + // TODO @AI:各种情况的翻译;看看怎么弄比较合适;是不是要用 fc 自定义的 callback 下? + log.error("[handleCustomFrame][解析自定义 FC 数据失败]", e); + } + } + + /** + * 处理认证请求 + */ + private void handleAuth(NetSocket socket, IotModbusFrame frame, JSONObject json, + IotModbusFrameFormatEnum frameFormat) { + // TODO @AI:参数为空的校验; + JSONObject params = json.getJSONObject("params"); + if (params == null) { + sendAuthResponse(socket, frame, frameFormat, 1, "params 为空"); + return; + } + // TODO @AI:参数判空; + String clientId = params.getStr("clientId"); + String username = params.getStr("username"); + String password = params.getStr("password"); + + try { + // 1. 调用认证 API + IotDeviceAuthReqDTO authReq = new IotDeviceAuthReqDTO() + .setClientId(clientId).setUsername(username).setPassword(password); + CommonResult authResult = deviceApi.authDevice(authReq); + // TODO @AI:应该不用 close 吧?! + // TODO @AI:BooleanUtils.isFalse + if (authResult == null || !authResult.isSuccess() || !Boolean.TRUE.equals(authResult.getData())) { + log.warn("[handleAuth][认证失败, clientId={}, username={}]", clientId, username); + sendAuthResponse(socket, frame, frameFormat, 1, "认证失败"); + socket.close(); + return; + } + + // 2. 认证成功,查找设备配置(通过 username 作为 deviceName 查找) + // TODO 根据实际的认证模型优化查找逻辑 + // TODO @AI:通过 device + IotModbusDeviceConfigRespDTO config = configCacheService.findConfigByAuth(clientId, username, password); + if (config == null) { + // 退而求其次,遍历缓存查找 + log.info("[handleAuth][认证成功但未找到设备配置, clientId={}, username={}]", clientId, username); + } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + // TODO @AI:这里要优化下,不要通过 spring 这样注入; + IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + // TODO @AI:校验 frameFormat 是否一致;不一致,连接也失败; + + // 3. 注册连接 + ConnectionInfo connectionInfo = new ConnectionInfo() + .setDeviceId(device.getId()) + .setSlaveId(frame.getSlaveId()) + .setFrameFormat(frameFormat) + .setMode(config != null ? config.getMode() : IotModbusModeEnum.POLLING.getMode()); + + if (config != null) { + connectionInfo.setDeviceId(config.getDeviceId()) + .setProductKey(config.getProductKey()) + .setDeviceName(config.getDeviceName()); + } + connectionManager.registerConnection(socket, connectionInfo); + + // 4. 发送认证成功响应 + sendAuthResponse(socket, frame, frameFormat, 0, "success"); + log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", clientId, + config != null ? config.getDeviceId() : "unknown"); + + // 5. 回调:启动轮询等 + // TODO @AI:是不是不要 callback,而是主动调用! + if (onAuthSuccess != null && config != null) { + onAuthSuccess.accept(config.getDeviceId(), config); + } + } catch (Exception e) { + log.error("[handleAuth][认证异常]", e); + sendAuthResponse(socket, frame, frameFormat, 1, "认证异常"); + socket.close(); + } + } + + /** + * 发送认证响应 + */ + private void sendAuthResponse(NetSocket socket, IotModbusFrame frame, + IotModbusFrameFormatEnum frameFormat, + int code, String message) { + // TODO @AI:不一定用 auth response;而是 custom? + JSONObject resp = new JSONObject(); + resp.set("method", "auth"); + resp.set("code", code); + resp.set("message", message); + byte[] data = frameCodec.encodeCustomFrame(frame.getSlaveId(), resp.toString(), + frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0); + connectionManager.sendToSocket(socket, data); + } + + /** + * 处理轮询响应(mode=1) + */ + private void handlePollingResponse(ConnectionInfo info, IotModbusFrame frame, + IotModbusFrameFormatEnum frameFormat) { + // 1.1 匹配 PendingRequest + PendingRequest request = pendingRequestManager.matchResponse( + info.getDeviceId(), frame, frameFormat); + if (request == null) { + log.debug("[handlePollingResponse][未匹配到 PendingRequest, deviceId={}, FC={}]", + info.getDeviceId(), frame.getFunctionCode()); + return; + } + // 1.2 提取寄存器值 + int[] rawValues = IotModbusResponseParser.extractValues(frame); + if (rawValues == null) { + log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]", + info.getDeviceId(), request.getIdentifier()); + return; + } + // 1.3 查找点位配置 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId()); + if (config == null || config.getPoints() == null) { + return; + } + // TODO @AI:findone arrayUtil; + var point = config.getPoints().stream() + .filter(p -> p.getId().equals(request.getPointId())) + .findFirst().orElse(null); + if (point == null) { + return; + } + + // TODO @AI:拆成 2.1、2.2 + // 4. 点位翻译 → 上报 + Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point); + Map params = MapUtil.of(request.getIdentifier(), convertedValue); + IotDeviceMessage message = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); + messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId); + log.debug("[handlePollingResponse][设备={}, 属性={}, 原始值={}, 转换值={}]", + info.getDeviceId(), request.getIdentifier(), rawValues, convertedValue); + } + + // TODO @AI:不需要这个逻辑; + /** + * 处理主动上报(mode=2) + * 设备直接上报 property.report 格式:{propertyId: value},不做点位翻译 + */ + @SuppressWarnings("unchecked") + private void handleActiveReport(ConnectionInfo info, IotModbusFrame frame) { + // mode=2 下设备上报标准 Modbus 帧,但由于没有点位翻译, + // 这里暂时将原始寄存器值以 FC+地址 为 key 上报 + int[] rawValues = IotModbusResponseParser.extractValues(frame); + if (rawValues == null) { + return; + } + + // 简单上报:以 "register_FC{fc}" 作为属性名 + String propertyKey = "register_FC" + frame.getFunctionCode(); + Map params = MapUtil.of(propertyKey, rawValues); + IotDeviceMessage message = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); + messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId); + + log.debug("[handleActiveReport][设备={}, FC={}, 原始值={}]", + info.getDeviceId(), frame.getFunctionCode(), rawValues); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java new file mode 100644 index 0000000000..382e8ab56e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java @@ -0,0 +1,172 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.math.BigDecimal; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +// TODO @AI:和 IotModbusTcpConfigCacheService 基本一致?! +/** + * IoT Modbus TCP Slave 配置缓存服务 + *

                              + * 负责:从 biz 拉取 Modbus 设备配置,缓存配置数据,并检测配置变更 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpSlaveConfigCacheService { + + private final IotDeviceCommonApi deviceApi; + + /** + * 配置缓存:deviceId -> 配置 + */ + private final Map configCache = new ConcurrentHashMap<>(); + + // TODO @AI:它的 diff 算法,是不是不用和 IotModbusTcpConfigCacheService 完全一致;更多是1)首次连接时,查找;2)断开连接,移除;3)定时轮询更新; + /** + * 已知的设备 ID 集合 + */ + private final Set knownDeviceIds = ConcurrentHashMap.newKeySet(); + + /** + * 刷新配置 + * + * @return 最新的配置列表 + */ + public List refreshConfig() { + try { + // 1. 从远程获取配置 + // TODO @AI:需要过滤下,只查找连接的设备列表;并且只有主动轮询的,才会处理;方法名,应该是 List 结尾; + CommonResult> result = deviceApi.getEnabledModbusDeviceConfigs(); + if (result == null || !result.isSuccess() || result.getData() == null) { + log.warn("[refreshConfig][获取 Modbus 配置失败: {}]", result); + return new ArrayList<>(configCache.values()); + } + List configs = new ArrayList<>(result.getData()); + + // 2. 追加 Mock 测试数据(一次性测试用途) + // TODO @芋艿:测试完成后移除 + configs.addAll(buildMockConfigs()); + + // 3. 更新缓存 + for (IotModbusDeviceConfigRespDTO config : configs) { + configCache.put(config.getDeviceId(), config); + } + return configs; + } catch (Exception e) { + log.error("[refreshConfig][刷新配置失败]", e); + return new ArrayList<>(configCache.values()); + } + } + + /** + * 构建 Mock 测试配置数据(一次性测试用途) + * + * 设备:PRODUCT_KEY=4aymZgOTOOCrDKRT, DEVICE_NAME=small + * 点位:temperature(FC03, 地址 0)、humidity(FC03, 地址 1) + * + * TODO @芋艿:测试完成后移除 + */ + private List buildMockConfigs() { + IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO(); + config.setDeviceId(1L); + config.setProductKey("4aymZgOTOOCrDKRT"); + config.setDeviceName("small"); + config.setSlaveId(1); + config.setMode(1); // 云端轮询 + config.setFrameFormat("modbus_tcp"); + + // 点位列表 + List points = new ArrayList<>(); + + // 点位 1:温度 - 保持寄存器 FC03, 地址 0, 1 个寄存器, INT16 + IotModbusPointRespDTO point1 = new IotModbusPointRespDTO(); + point1.setId(1L); + point1.setIdentifier("temperature"); + point1.setName("温度"); + point1.setFunctionCode(3); // FC03 读保持寄存器 + point1.setRegisterAddress(0); + point1.setRegisterCount(1); + point1.setRawDataType("INT16"); + point1.setByteOrder("BIG_ENDIAN"); + point1.setScale(new BigDecimal("0.1")); + point1.setPollInterval(5000); // 5 秒轮询一次 + points.add(point1); + + // 点位 2:湿度 - 保持寄存器 FC03, 地址 1, 1 个寄存器, UINT16 + IotModbusPointRespDTO point2 = new IotModbusPointRespDTO(); + point2.setId(2L); + point2.setIdentifier("humidity"); + point2.setName("湿度"); + point2.setFunctionCode(3); // FC03 读保持寄存器 + point2.setRegisterAddress(1); + point2.setRegisterCount(1); + point2.setRawDataType("UINT16"); + point2.setByteOrder("BIG_ENDIAN"); + point2.setScale(new BigDecimal("0.1")); + point2.setPollInterval(5000); // 5 秒轮询一次 + points.add(point2); + + config.setPoints(points); + log.info("[buildMockConfigs][已加载 Mock 配置, deviceId={}, points={}]", config.getDeviceId(), points.size()); + return Collections.singletonList(config); + } + + /** + * 获取设备配置 + */ + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { + return configCache.get(deviceId); + } + + // TODO @AI:这个逻辑,是不是非必须? + /** + * 通过 clientId + username + password 查找设备配置(认证用) + * 暂通过遍历缓存实现,后续可优化为索引 + */ + public IotModbusDeviceConfigRespDTO findConfigByAuth(String clientId, String username, String password) { + // TODO @芋艿:测试完成后移除 mock 逻辑,改为正式查找 + // Mock:通过 clientId(格式 productKey.deviceName)匹配缓存中的设备 + if (clientId != null && clientId.contains(".")) { + String[] parts = clientId.split("\\.", 2); + String productKey = parts[0]; + String deviceName = parts[1]; + for (IotModbusDeviceConfigRespDTO config : configCache.values()) { + if (productKey.equals(config.getProductKey()) && deviceName.equals(config.getDeviceName())) { + return config; + } + } + } + return null; + } + + /** + * 清理已删除设备的资源 + */ + public void cleanupRemovedDevices(List currentConfigs, Consumer cleanupAction) { + Set currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId); + Set removedDeviceIds = new HashSet<>(knownDeviceIds); + removedDeviceIds.removeAll(currentDeviceIds); + + for (Long deviceId : removedDeviceIds) { + log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); + configCache.remove(deviceId); + cleanupAction.accept(deviceId); + } + + knownDeviceIds.clear(); + knownDeviceIds.addAll(currentDeviceIds); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java new file mode 100644 index 0000000000..b342158a15 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java @@ -0,0 +1,160 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; + +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetSocket; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT Modbus TCP Slave 连接管理器 + *

                              + * 管理设备 TCP 连接:socket ↔ 设备双向映射 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpSlaveConnectionManager { + + /** + * socket → 连接信息 + */ + private final Map connectionMap = new ConcurrentHashMap<>(); + + /** + * deviceId → socket + */ + private final Map deviceSocketMap = new ConcurrentHashMap<>(); + + /** + * 连接信息 + */ + @Data + public static class ConnectionInfo { + + /** + * 设备编号 + */ + private Long deviceId; + /** + * 产品标识 + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; + /** + * 从站地址 + */ + private Integer slaveId; + + /** + * 帧格式(首帧自动检测得到) + */ + private IotModbusFrameFormatEnum frameFormat; + + // TODO @AI:mode 是否非必须?! + /** + * 模式:1-云端轮询 2-主动上报 + */ + private Integer mode; + + } + + /** + * 注册已认证的连接 + */ + public void registerConnection(NetSocket socket, ConnectionInfo info) { + connectionMap.put(socket, info); + deviceSocketMap.put(info.getDeviceId(), socket); + log.info("[registerConnection][设备 {} 连接已注册, remoteAddress={}]", + info.getDeviceId(), socket.remoteAddress()); + } + + // TODO @芋艿:待定是不是要保留?! + /** + * 设置连接的帧格式(首帧检测后调用) + */ + public void setFrameFormat(NetSocket socket, IotModbusFrameFormatEnum frameFormat) { + ConnectionInfo info = connectionMap.get(socket); + if (info != null) { + info.setFrameFormat(frameFormat); + } + } + + /** + * 获取连接信息 + */ + public ConnectionInfo getConnectionInfo(NetSocket socket) { + return connectionMap.get(socket); + } + + /** + * 根据设备 ID 获取连接信息 + */ + public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) { + NetSocket socket = deviceSocketMap.get(deviceId); + return socket != null ? connectionMap.get(socket) : null; + } + + // TODO @AI:不用判断连接是否认证; + /** + * 判断连接是否已认证 + */ + public boolean isAuthenticated(NetSocket socket) { + return connectionMap.containsKey(socket); + } + + /** + * 移除连接 + */ + public ConnectionInfo removeConnection(NetSocket socket) { + ConnectionInfo info = connectionMap.remove(socket); + if (info != null && info.getDeviceId() != null) { + deviceSocketMap.remove(info.getDeviceId()); + log.info("[removeConnection][设备 {} 连接已移除]", info.getDeviceId()); + } + return info; + } + + /** + * 发送数据到设备 + */ + public void sendToDevice(Long deviceId, byte[] data) { + NetSocket socket = deviceSocketMap.get(deviceId); + if (socket == null) { + log.warn("[sendToDevice][设备 {} 没有连接]", deviceId); + return; + } + // TODO @AI:直接复用 sendToSocket 方法?! + socket.write(Buffer.buffer(data)); + } + + /** + * 发送数据到指定 socket + */ + public void sendToSocket(NetSocket socket, byte[] data) { + socket.write(Buffer.buffer(data)); + } + + // TODO @AI:貌似别的都没这个,是不是可以去掉哈?! + /** + * 关闭所有连接 + */ + public void closeAll() { + for (NetSocket socket : connectionMap.keySet()) { + try { + socket.close(); + } catch (Exception e) { + log.error("[closeAll][关闭连接失败]", e); + } + } + connectionMap.clear(); + deviceSocketMap.clear(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java new file mode 100644 index 0000000000..f82ddbfcae --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java @@ -0,0 +1,151 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; + +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.Deque; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; + +/** + * IoT Modbus TCP Slave 待响应请求管理器 + *

                              + * 管理轮询下发的请求,用于匹配设备响应: + * - TCP 模式:按 transactionId 精确匹配 + * - RTU 模式:按 slaveId + functionCode FIFO 匹配 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpSlavePendingRequestManager { + + /** + * deviceId → 有序队列 + */ + private final Map> pendingRequests = new ConcurrentHashMap<>(); + + /** + * 待响应请求信息 + */ + @Data + @AllArgsConstructor + public static class PendingRequest { + + private Long deviceId; + private Long pointId; + private String identifier; + private int slaveId; + private int functionCode; + private int registerAddress; + private int registerCount; + private Integer transactionId; + private long expireAt; + + } + + /** + * 添加待响应请求 + */ + public void addRequest(PendingRequest request) { + pendingRequests.computeIfAbsent(request.getDeviceId(), k -> new ConcurrentLinkedDeque<>()) + .addLast(request); + } + + /** + * 匹配响应(TCP 模式按 transactionId,RTU 模式按 FIFO) + * + * @param deviceId 设备 ID + * @param frame 收到的响应帧 + * @param frameFormat 帧格式 + * @return 匹配到的 PendingRequest,没有匹配返回 null + */ + public PendingRequest matchResponse(Long deviceId, IotModbusFrame frame, + IotModbusFrameFormatEnum frameFormat) { + Deque queue = pendingRequests.get(deviceId); + // TODO @AI:CollUtil.isEmpty(queue) + if (queue == null || queue.isEmpty()) { + return null; + } + + if (frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP && frame.getTransactionId() != null) { + // TCP 模式:按 transactionId 精确匹配 + return matchByTransactionId(queue, frame.getTransactionId()); + } else { + // RTU 模式:FIFO,匹配 slaveId + functionCode + return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode()); + } + } + + /** + * 按 transactionId 匹配 + */ + private PendingRequest matchByTransactionId(Deque queue, int transactionId) { + // TODO @AI:需要兼容 jdk8; + for (var it = queue.iterator(); it.hasNext(); ) { + PendingRequest req = it.next(); + if (req.getTransactionId() != null && req.getTransactionId() == transactionId) { + it.remove(); + return req; + } + } + return null; + } + + /** + * 按 FIFO 匹配 + */ + private PendingRequest matchByFifo(Deque queue, int slaveId, int functionCode) { + // TODO @AI:需要兼容 jdk8; + for (var it = queue.iterator(); it.hasNext(); ) { + PendingRequest req = it.next(); + if (req.getSlaveId() == slaveId && req.getFunctionCode() == functionCode) { + it.remove(); + return req; + } + } + return null; + } + + /** + * 清理过期请求 + */ + public void cleanupExpired() { + long now = System.currentTimeMillis(); + for (Map.Entry> entry : pendingRequests.entrySet()) { + Deque queue = entry.getValue(); + int removed = 0; + while (!queue.isEmpty()) { + PendingRequest req = queue.peekFirst(); + // TODO @AI:if return 减少括号层级; + if (req != null && req.getExpireAt() < now) { + queue.pollFirst(); + removed++; + } else { + break; // 队列有序,后面的没过期 + } + } + if (removed > 0) { + log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed); + } + } + } + + /** + * 清理指定设备的所有待响应请求 + */ + public void removeDevice(Long deviceId) { + pendingRequests.remove(deviceId); + } + + /** + * 清理所有待响应请求 + */ + public void clear() { + pendingRequests.clear(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java new file mode 100644 index 0000000000..51246105c6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java @@ -0,0 +1,207 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; +import io.vertx.core.Vertx; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +// TODO @AI:和 IotModbusTcpPollScheduler 很像,是不是可以做一些复用? +/** + * IoT Modbus TCP Slave 轮询调度器 + *

                              + * 管理点位的轮询定时器,为 mode=1(云端轮询)的设备调度读取任务。 + * 与 tcpmaster 不同,这里不直接通过 j2mod 读取,而是: + * 1. 编码 Modbus 读请求帧 + * 2. 通过 ConnectionManager 发送到设备的 TCP 连接 + * 3. 将请求注册到 PendingRequestManager,等待设备响应 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpSlavePollScheduler { + + private final Vertx vertx; + private final IotModbusTcpSlaveConnectionManager connectionManager; + private final IotModbusFrameCodec frameCodec; + private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; + private final int requestTimeout; + + /** + * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) + */ + private final Map> devicePointTimers = new ConcurrentHashMap<>(); + + /** + * TCP 事务 ID 自增器 + */ + private final AtomicInteger transactionIdCounter = new AtomicInteger(0); + + /** + * 点位定时器信息 + */ + @Data + @AllArgsConstructor + private static class PointTimerInfo { + + /** + * Vert.x 定时器 ID + */ + private Long timerId; + /** + * 轮询间隔(用于判断是否需要更新定时器) + */ + private Integer pollInterval; + + } + + /** + * 更新轮询任务(增量更新) + */ + public void updatePolling(IotModbusDeviceConfigRespDTO config) { + Long deviceId = config.getDeviceId(); + List newPoints = config.getPoints(); + Map currentTimers = devicePointTimers + .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>()); + // 1.1 计算新配置中的点位 ID 集合 + Set newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId); + // 1.2 计算删除的点位 ID 集合 + Set removedPointIds = new HashSet<>(currentTimers.keySet()); + removedPointIds.removeAll(newPointIds); + + // 2. 处理删除的点位:停止不再存在的定时器 + for (Long pointId : removedPointIds) { + PointTimerInfo timerInfo = currentTimers.remove(pointId); + if (timerInfo != null) { + vertx.cancelTimer(timerInfo.getTimerId()); + log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId); + } + } + + // 3. 处理新增和修改的点位 + if (CollUtil.isEmpty(newPoints)) { + return; + } + for (IotModbusPointRespDTO point : newPoints) { + Long pointId = point.getId(); + Integer newPollInterval = point.getPollInterval(); + PointTimerInfo existingTimer = currentTimers.get(pointId); + // 3.1 新增点位:创建定时器 + if (existingTimer == null) { + Long timerId = createPollTimer(config, point); + if (timerId != null) { + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); + log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", + deviceId, pointId, newPollInterval); + } + } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { + // 3.2 pollInterval 变化:重建定时器 + vertx.cancelTimer(existingTimer.getTimerId()); + Long timerId = createPollTimer(config, point); + if (timerId != null) { + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); + log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", + deviceId, pointId, existingTimer.getPollInterval(), newPollInterval); + } else { + currentTimers.remove(pointId); + } + } + // 3.3 其他属性变化:不处理(下次轮询时自动使用新配置) + } + } + + /** + * 创建轮询定时器 + */ + private Long createPollTimer(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { + if (point.getPollInterval() == null || point.getPollInterval() <= 0) { + return null; + } + return vertx.setPeriodic(point.getPollInterval(), timerId -> { + try { + pollPoint(config, point); + } catch (Exception e) { + log.error("[createPollTimer][轮询点位失败, deviceId={}, identifier={}]", + config.getDeviceId(), point.getIdentifier(), e); + } + }); + } + + /** + * 轮询单个点位:编码读请求帧 → 发送 → 注册 PendingRequest + */ + private void pollPoint(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { + Long deviceId = config.getDeviceId(); + // 1. 获取连接信息 + ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(deviceId); + if (connInfo == null) { + log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId); + return; + } + + // 2.1 确定帧格式和事务 ID + // TODO @AI:不允许为空! + IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); + if (frameFormat == null) { + frameFormat = IotModbusFrameFormatEnum.MODBUS_TCP; + } + // TODO @AI:transactionId 需要根据设备来么?然后递增也根据 IotModbusFrameFormatEnum.MODBUS_TCP 提前判断; + int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; + int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; + // 2.2 编码读请求 + byte[] data = frameCodec.encodeReadRequest(slaveId, point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId); + // 2.3 注册 PendingRequest + PendingRequest pendingRequest = new PendingRequest( + deviceId, point.getId(), point.getIdentifier(), + slaveId, point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount(), + frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP ? transactionId : null, + System.currentTimeMillis() + requestTimeout); + pendingRequestManager.addRequest(pendingRequest); + + // 3. 发送读请求 + connectionManager.sendToDevice(deviceId, data); + log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]", + deviceId, point.getIdentifier(), point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount()); + } + + /** + * 停止设备的轮询 + */ + public void stopPolling(Long deviceId) { + Map timers = devicePointTimers.remove(deviceId); + if (CollUtil.isEmpty(timers)) { + return; + } + for (PointTimerInfo timerInfo : timers.values()) { + vertx.cancelTimer(timerInfo.getTimerId()); + } + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); + } + + /** + * 停止所有轮询 + */ + public void stopAll() { + for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) { + stopPolling(deviceId); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 77323e03b1..e2cfd94abb 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -164,14 +164,26 @@ yudao: trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 # ==================================== - # 针对引入的 Modbus TCP 组件的配置 + # 针对引入的 Modbus TCP Master 组件的配置 # ==================================== - - id: modbus-tcp-1 - enabled: true - protocol: modbus_tcp + - id: modbus-tcp-master-1 + enabled: false + protocol: modbus_tcp_master port: 502 - modbus-tcp: + modbus-tcp-master: config-refresh-interval: 30 # 配置刷新间隔(秒) + # ==================================== + # 针对引入的 Modbus TCP Slave 组件的配置 + # ==================================== + - id: modbus-tcp-slave-1 + enabled: true + protocol: modbus_tcp_slave + port: 503 + modbus-tcp-slave: + config-refresh-interval: 30 # 配置刷新间隔(秒) + custom-function-code: 65 # 自定义功能码(用于认证等扩展交互) + request-timeout: 5000 # Pending Request 超时时间(毫秒) + request-cleanup-interval: 10000 # Pending Request 清理间隔(毫秒) --- #################### 日志相关配置 #################### @@ -193,7 +205,7 @@ logging: cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG - cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp: DEBUG + cn.iocoder.yudao.module.iot.gateway.protocol.modbus: DEBUG # 根日志级别 root: INFO diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/ModbusRtuOverTcpDemo.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/ModbusRtuOverTcpDemo.java new file mode 100644 index 0000000000..80d4c91199 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/ModbusRtuOverTcpDemo.java @@ -0,0 +1,304 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus; + +import com.ghgande.j2mod.modbus.io.ModbusRTUTCPTransport; +import com.ghgande.j2mod.modbus.msg.*; +import com.ghgande.j2mod.modbus.procimg.*; +import com.ghgande.j2mod.modbus.slave.ModbusSlave; +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; + +import java.net.ServerSocket; +import java.net.Socket; + +/** + * Modbus RTU over TCP 完整 Demo + * + * 架构:Master(主站)启动 TCP Server 监听 → Slave(从站)主动 TCP 连接上来 + * 通信协议:RTU 帧格式(带 CRC)通过 TCP 传输,而非标准 MBAP 头 + * + * 流程: + * 1. Master 启动 TCP ServerSocket 监听端口 + * 2. Slave(从站模拟器)作为 TCP Client 连接到 Master + * 3. Master 通过 accept 得到的 Socket,使用 {@link ModbusRTUTCPTransport} 发送读写请求 + * + * 实现说明: + * 因为 j2mod 的 ModbusSlave 只能以 TCP Server 模式运行(监听端口等待 Master 连接), + * 不支持"Slave 作为 TCP Client 主动连接 Master"的模式。 + * 所以这里用一个 TCP 桥接(bridge)来模拟: + * - Slave 在本地内部端口启动(RTU over TCP 模式) + * - 一个桥接线程同时连接 Master Server 和 Slave 内部端口,做双向数据转发 + * - Master 视角:看到的是 Slave 主动连上来 + * + * 依赖:j2mod 3.2.1(pom.xml 中已声明) + * + * @author 芋道源码 + */ +@Deprecated // 仅技术演示,非是必须的 +public class ModbusRtuOverTcpDemo { + + /** + * Master(主站)TCP Server 监听端口 + */ + private static final int PORT = 5021; + /** + * Slave 内部端口(仅本地中转用,不对外暴露) + */ + private static final int SLAVE_INTERNAL_PORT = PORT + 100; + /** + * Modbus 从站地址 + */ + private static final int SLAVE_ID = 1; + + public static void main(String[] args) throws Exception { + // ===================== 第一步:Master 启动 TCP Server 监听 ===================== + ServerSocket serverSocket = new ServerSocket(PORT); + System.out.println("==================================================="); + System.out.println("[Master] TCP Server 已启动,监听端口: " + PORT); + System.out.println("[Master] 等待 Slave 连接..."); + System.out.println("==================================================="); + + // ===================== 第二步:后台启动 Slave,它会主动连接 Master ===================== + ModbusSlave slave = startSlaveInBackground(); + + // Master accept Slave 的连接 + Socket slaveSocket = serverSocket.accept(); + System.out.println("[Master] Slave 已连接: " + slaveSocket.getRemoteSocketAddress()); + + // ===================== 第三步:Master 通过 RTU over TCP 发送读写请求 ===================== + // 使用 ModbusRTUTCPTransport 包装 Socket(RTU 帧 = SlaveID + 功能码 + 数据 + CRC,无 MBAP 头) + ModbusRTUTCPTransport transport = new ModbusRTUTCPTransport(slaveSocket); + + try { + System.out.println("[Master] RTU over TCP 通道已建立\n"); + + // 1. 读操作演示:4 种功能码 + demoReadCoils(transport); // 功能码 01:读线圈 + demoReadDiscreteInputs(transport); // 功能码 02:读离散输入 + demoReadHoldingRegisters(transport); // 功能码 03:读保持寄存器 + demoReadInputRegisters(transport); // 功能码 04:读输入寄存器 + + // 2. 写操作演示 + 读回验证 + demoWriteCoil(transport); // 功能码 05:写单个线圈 + demoWriteRegister(transport); // 功能码 06:写单个保持寄存器 + + System.out.println("\n==================================================="); + System.out.println("所有 RTU over TCP 读写操作执行成功!"); + System.out.println("==================================================="); + } finally { + // 清理资源 + transport.close(); + slaveSocket.close(); + serverSocket.close(); + slave.close(); + System.out.println("[Master] 资源已关闭"); + } + } + + // ===================== Slave 设备模拟(作为 TCP Client 连接 Master) ===================== + + /** + * 在后台启动从站模拟器,并通过 TCP 桥接连到 Master Server + * + * @return ModbusSlave 实例(用于最后关闭资源) + */ + private static ModbusSlave startSlaveInBackground() throws Exception { + // 1. 创建进程映像,初始化寄存器数据 + SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID); + // 1.1 线圈(Coil,功能码 01/05)- 可读写,地址 0~9 + for (int i = 0; i < 10; i++) { + spi.addDigitalOut(new SimpleDigitalOut(i % 2 == 0)); + } + // 1.2 离散输入(Discrete Input,功能码 02)- 只读,地址 0~9 + for (int i = 0; i < 10; i++) { + spi.addDigitalIn(new SimpleDigitalIn(i % 3 == 0)); + } + // 1.3 保持寄存器(Holding Register,功能码 03/06/16)- 可读写,地址 0~19 + for (int i = 0; i < 20; i++) { + spi.addRegister(new SimpleRegister(i * 100)); + } + // 1.4 输入寄存器(Input Register,功能码 04)- 只读,地址 0~19 + for (int i = 0; i < 20; i++) { + spi.addInputRegister(new SimpleInputRegister(i * 10 + 1)); + } + + // 2. 启动 Slave(RTU over TCP 模式,在本地内部端口监听) + ModbusSlave slave = ModbusSlaveFactory.createTCPSlave(SLAVE_INTERNAL_PORT, 5, true); + slave.addProcessImage(SLAVE_ID, spi); + slave.open(); + System.out.println("[Slave] 从站模拟器已启动(内部端口: " + SLAVE_INTERNAL_PORT + ")"); + + // 3. 启动桥接线程:TCP Client 连接 Master Server,同时连接 Slave 内部端口,双向转发 + Thread bridgeThread = new Thread(() -> { + try { + Socket toMaster = new Socket("127.0.0.1", PORT); + Socket toSlave = new Socket("127.0.0.1", SLAVE_INTERNAL_PORT); + System.out.println("[Bridge] 已建立桥接: Master(" + PORT + ") <-> Slave(" + SLAVE_INTERNAL_PORT + ")"); + + // 双向桥接:Master ↔ Bridge ↔ Slave + Thread forward = new Thread(() -> bridge(toMaster, toSlave), "bridge-master→slave"); + Thread backward = new Thread(() -> bridge(toSlave, toMaster), "bridge-slave→master"); + forward.setDaemon(true); + backward.setDaemon(true); + forward.start(); + backward.start(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "bridge-setup"); + bridgeThread.setDaemon(true); + bridgeThread.start(); + + return slave; + } + + /** + * TCP 双向桥接:从 src 读取数据,写入 dst + */ + private static void bridge(Socket src, Socket dst) { + try { + byte[] buf = new byte[1024]; + var in = src.getInputStream(); + var out = dst.getOutputStream(); + int len; + while ((len = in.read(buf)) != -1) { + out.write(buf, 0, len); + out.flush(); + } + } catch (Exception ignored) { + // 连接关闭时正常退出 + } + } + + // ===================== Master 读写操作 ===================== + + /** + * 发送请求并接收响应(通用方法) + */ + private static ModbusResponse sendRequest(ModbusRTUTCPTransport transport, ModbusRequest request) throws Exception { + request.setUnitID(SLAVE_ID); + transport.writeRequest(request); + return transport.readResponse(); + } + + /** + * 功能码 01:读线圈(Read Coils) + */ + private static void demoReadCoils(ModbusRTUTCPTransport transport) throws Exception { + ReadCoilsRequest request = new ReadCoilsRequest(0, 5); + ReadCoilsResponse response = (ReadCoilsResponse) sendRequest(transport, request); + + StringBuilder sb = new StringBuilder("[功能码 01] 读线圈(0~4): "); + for (int i = 0; i < 5; i++) { + sb.append(response.getCoilStatus(i) ? "ON" : "OFF"); + if (i < 4) { + sb.append(", "); + } + } + System.out.println(sb); + } + + /** + * 功能码 02:读离散输入(Read Discrete Inputs) + */ + private static void demoReadDiscreteInputs(ModbusRTUTCPTransport transport) throws Exception { + ReadInputDiscretesRequest request = new ReadInputDiscretesRequest(0, 5); + ReadInputDiscretesResponse response = (ReadInputDiscretesResponse) sendRequest(transport, request); + + StringBuilder sb = new StringBuilder("[功能码 02] 读离散输入(0~4): "); + for (int i = 0; i < 5; i++) { + sb.append(response.getDiscreteStatus(i) ? "ON" : "OFF"); + if (i < 4) { + sb.append(", "); + } + } + System.out.println(sb); + } + + /** + * 功能码 03:读保持寄存器(Read Holding Registers) + */ + private static void demoReadHoldingRegisters(ModbusRTUTCPTransport transport) throws Exception { + ReadMultipleRegistersRequest request = new ReadMultipleRegistersRequest(0, 5); + ReadMultipleRegistersResponse response = (ReadMultipleRegistersResponse) sendRequest(transport, request); + + StringBuilder sb = new StringBuilder("[功能码 03] 读保持寄存器(0~4): "); + for (int i = 0; i < response.getWordCount(); i++) { + sb.append(response.getRegisterValue(i)); + if (i < response.getWordCount() - 1) { + sb.append(", "); + } + } + System.out.println(sb); + } + + /** + * 功能码 04:读输入寄存器(Read Input Registers) + */ + private static void demoReadInputRegisters(ModbusRTUTCPTransport transport) throws Exception { + ReadInputRegistersRequest request = new ReadInputRegistersRequest(0, 5); + ReadInputRegistersResponse response = (ReadInputRegistersResponse) sendRequest(transport, request); + + StringBuilder sb = new StringBuilder("[功能码 04] 读输入寄存器(0~4): "); + for (int i = 0; i < response.getWordCount(); i++) { + sb.append(response.getRegisterValue(i)); + if (i < response.getWordCount() - 1) { + sb.append(", "); + } + } + System.out.println(sb); + } + + /** + * 功能码 05:写单个线圈(Write Single Coil)+ 读回验证 + */ + private static void demoWriteCoil(ModbusRTUTCPTransport transport) throws Exception { + int address = 0; + + // 1. 先读取当前值 + ReadCoilsRequest readReq = new ReadCoilsRequest(address, 1); + ReadCoilsResponse readResp = (ReadCoilsResponse) sendRequest(transport, readReq); + boolean beforeValue = readResp.getCoilStatus(0); + + // 2. 写入相反的值 + boolean writeValue = !beforeValue; + WriteCoilRequest writeReq = new WriteCoilRequest(address, writeValue); + sendRequest(transport, writeReq); + + // 3. 读回验证 + ReadCoilsResponse verifyResp = (ReadCoilsResponse) sendRequest(transport, readReq); + boolean afterValue = verifyResp.getCoilStatus(0); + + System.out.println("[功能码 05] 写线圈: 地址=" + address + + ", 写入前=" + (beforeValue ? "ON" : "OFF") + + ", 写入值=" + (writeValue ? "ON" : "OFF") + + ", 读回值=" + (afterValue ? "ON" : "OFF") + + (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败")); + } + + /** + * 功能码 06:写单个保持寄存器(Write Single Register)+ 读回验证 + */ + private static void demoWriteRegister(ModbusRTUTCPTransport transport) throws Exception { + int address = 0; + int writeValue = 12345; + + // 1. 先读取当前值 + ReadMultipleRegistersRequest readReq = new ReadMultipleRegistersRequest(address, 1); + ReadMultipleRegistersResponse readResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq); + int beforeValue = readResp.getRegisterValue(0); + + // 2. 写入新值 + WriteSingleRegisterRequest writeReq = new WriteSingleRegisterRequest(address, new SimpleRegister(writeValue)); + sendRequest(transport, writeReq); + + // 3. 读回验证 + ReadMultipleRegistersResponse verifyResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq); + int afterValue = verifyResp.getRegisterValue(0); + + System.out.println("[功能码 06] 写保持寄存器: 地址=" + address + + ", 写入前=" + beforeValue + + ", 写入值=" + writeValue + + ", 读回值=" + afterValue + + (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败")); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/ModbusTcpSlaveSimulatorTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/ModbusTcpSlaveSimulatorTest.java similarity index 98% rename from yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/ModbusTcpSlaveSimulatorTest.java rename to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/ModbusTcpSlaveSimulatorTest.java index 509e600f00..f901bc3973 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/ModbusTcpSlaveSimulatorTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/ModbusTcpSlaveSimulatorTest.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; import com.ghgande.j2mod.modbus.procimg.*; import com.ghgande.j2mod.modbus.slave.ModbusSlave; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java new file mode 100644 index 0000000000..0580909d05 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java @@ -0,0 +1,309 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusRecordParserFactory; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * IoT Modbus TCP Slave 协议集成测试 — MODBUS_RTU 帧格式(手动测试) + * + *

                              测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_RTU(CRC16)帧格式通信 + * + *

                              使用步骤: + *

                                + *
                              1. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-slave 协议,默认端口 503)
                              2. + *
                              3. 确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_rtu)
                              4. + *
                              5. 运行以下测试方法: + *
                                  + *
                                • {@link #testAuth()} - 自定义功能码认证
                                • + *
                                • {@link #testPollingResponse()} - 轮询响应
                                • + *
                                • {@link #testPropertySetWrite()} - 属性设置(接收写指令)
                                • + *
                                + *
                              6. + *
                              + * + * @author 芋道源码 + */ +@Slf4j +@Disabled +public class IotModbusTcpSlaveModbusRtuIntegrationTest { + + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 503; + private static final int TIMEOUT_MS = 5000; + + private static final int CUSTOM_FC = 65; + private static final int SLAVE_ID = 1; + + private static Vertx vertx; + private static NetClient netClient; + + // ===================== 编解码器 ===================== + + private static final IotModbusFrameCodec FRAME_CODEC = new IotModbusFrameCodec(CUSTOM_FC); + + // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== + + private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; + private static final String DEVICE_NAME = "small"; + private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; + + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + + // ===================== 认证测试 ===================== + + /** + * 认证测试:发送自定义功能码 FC65 认证帧(RTU 格式),验证认证成功响应 + */ + @Test + public void testAuth() throws Exception { + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + // 1. 构造并发送认证帧 + IotModbusFrame response = authenticate(socket); + + // 2. 验证响应 + log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]", + response.getSlaveId(), response.getFunctionCode(), response.getCustomData()); + JSONObject json = JSONUtil.parseObj(response.getCustomData()); + log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message")); + } finally { + socket.close(); + } + } + + // ===================== 轮询响应测试 ===================== + + /** + * 轮询响应测试:认证后等待网关下发 FC03 读请求(RTU 格式),构造读响应帧发回 + */ + @Test + public void testPollingResponse() throws Exception { + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + // 1. 先认证 + IotModbusFrame authResponse = authenticate(socket); + log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); + + // 2. 等待网关下发读请求 + log.info("[testPollingResponse][等待网关下发读请求...]"); + IotModbusFrame readRequest = waitForRequest(socket); + log.info("[testPollingResponse][收到读请求: slaveId={}, FC={}]", + readRequest.getSlaveId(), readRequest.getFunctionCode()); + + // 3. 解析读请求中的起始地址和数量 + byte[] pdu = readRequest.getPdu(); + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); + + // 4. 构造读响应帧(模拟寄存器数据,RTU 格式) + int[] registerValues = new int[quantity]; + for (int i = 0; i < quantity; i++) { + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... + } + byte[] responseData = buildReadResponse(readRequest.getSlaveId(), + readRequest.getFunctionCode(), registerValues); + socket.write(Buffer.buffer(responseData)); + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); + + // 5. 等待一段时间让网关处理 + Thread.sleep(20000); + } finally { + socket.close(); + } + } + + // ===================== 属性设置测试 ===================== + + /** + * 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求(RTU 格式) + *

                              + * 注意:需手动在平台触发 property.set + */ + @Test + public void testPropertySetWrite() throws Exception { + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + // 1. 先认证 + IotModbusFrame authResponse = authenticate(socket); + log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData()); + + // 2. 等待网关下发写请求(需手动在平台触发 property.set) + log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]"); + IotModbusFrame writeRequest = waitForRequest(socket); + log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, pdu={}]", + writeRequest.getSlaveId(), writeRequest.getFunctionCode(), + bytesToHex(writeRequest.getPdu())); + } finally { + socket.close(); + } + } + + // ===================== 辅助方法 ===================== + + /** + * 建立 TCP 连接 + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + + /** + * 执行认证并返回响应帧 + */ + private IotModbusFrame authenticate(NetSocket socket) throws Exception { + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); + return sendAndReceive(socket, authFrame); + } + + /** + * 发送帧并等待响应(使用 IotModbusRecordParserFactory 自动检测帧格式) + */ + private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { + CompletableFuture responseFuture = new CompletableFuture<>(); + // 使用 RecordParserFactory 创建拆包器(自动检测帧格式) + RecordParser parser = IotModbusRecordParserFactory.create(CUSTOM_FC, + buffer -> { + try { + // 检测到的帧格式应该是 RTU,使用 RTU 格式解码 + IotModbusFrame frame = FRAME_CODEC.decodeResponse( + buffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU); + responseFuture.complete(frame); + } catch (Exception e) { + responseFuture.completeExceptionally(e); + } + }, + format -> log.info("[sendAndReceive][检测到帧格式: {}]", format)); + socket.handler(parser); + + // 发送请求 + log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length); + socket.write(Buffer.buffer(frameData)); + + // 等待响应 + return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + /** + * 等待接收网关下发的请求帧(不发送,只等待接收) + */ + private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { + CompletableFuture requestFuture = new CompletableFuture<>(); + // 使用 RecordParserFactory 创建拆包器 + RecordParser parser = IotModbusRecordParserFactory.create(CUSTOM_FC, + buffer -> { + try { + IotModbusFrame frame = FRAME_CODEC.decodeResponse( + buffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU); + requestFuture.complete(frame); + } catch (Exception e) { + requestFuture.completeExceptionally(e); + } + }, + format -> log.info("[waitForRequest][检测到帧格式: {}]", format)); + socket.handler(parser); + + // 等待(超时 30 秒,因为轮询间隔可能比较长) + return requestFuture.get(30000, TimeUnit.MILLISECONDS); + } + + /** + * 构造认证帧(MODBUS_RTU 格式) + *

                              + * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}} + *

                              + * RTU 帧格式:[SlaveId(1)] [FC=0x41(1)] [ByteCount(1)] [JSON(N)] [CRC16(2)] + */ + private byte[] buildAuthFrame(String clientId, String username, String password) { + JSONObject params = new JSONObject(); + params.set("clientId", clientId); + params.set("username", username); + params.set("password", password); + JSONObject json = new JSONObject(); + json.set("method", "auth"); + json.set("params", params); + return FRAME_CODEC.encodeCustomFrame(SLAVE_ID, json.toString(), + IotModbusFrameFormatEnum.MODBUS_RTU, 0); + } + + /** + * 构造 FC03/FC01-04 读响应帧(MODBUS_RTU 格式) + *

                              + * RTU 帧格式:[SlaveId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)] [CRC16(2)] + */ + private byte[] buildReadResponse(int slaveId, int functionCode, int[] registerValues) { + int byteCount = registerValues.length * 2; + // 帧长度:SlaveId(1) + FC(1) + ByteCount(1) + Data(N*2) + CRC(2) + int totalLength = 1 + 1 + 1 + byteCount + 2; + byte[] frame = new byte[totalLength]; + frame[0] = (byte) slaveId; + frame[1] = (byte) functionCode; + frame[2] = (byte) byteCount; + for (int i = 0; i < registerValues.length; i++) { + frame[3 + i * 2] = (byte) ((registerValues[i] >> 8) & 0xFF); + frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF); + } + // 计算 CRC16 + int crc = IotModbusFrameCodec.calculateCrc16(frame, totalLength - 2); + frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low + frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High + return frame; + } + + /** + * 字节数组转十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + if (bytes == null) { + return "null"; + } + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X ", b)); + } + return sb.toString().trim(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java new file mode 100644 index 0000000000..4d249927e7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java @@ -0,0 +1,334 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * IoT Modbus TCP Slave 协议集成测试 — MODBUS_TCP 帧格式(手动测试) + * + *

                              测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_TCP(MBAP 头)帧格式通信 + * + *

                              使用步骤: + *

                                + *
                              1. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-slave 协议,默认端口 503)
                              2. + *
                              3. 确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_tcp)
                              4. + *
                              5. 运行以下测试方法: + *
                                  + *
                                • {@link #testAuth()} - 自定义功能码认证
                                • + *
                                • {@link #testPollingResponse()} - 轮询响应
                                • + *
                                • {@link #testPropertySetWrite()} - 属性设置(接收写指令)
                                • + *
                                + *
                              6. + *
                              + * + * @author 芋道源码 + */ +@Slf4j +@Disabled +public class IotModbusTcpSlaveModbusTcpIntegrationTest { + + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 503; + private static final int TIMEOUT_MS = 5000; + + private static final int CUSTOM_FC = 65; + private static final int SLAVE_ID = 1; + + private static Vertx vertx; + private static NetClient netClient; + + // ===================== 编解码器 ===================== + + private static final IotModbusFrameCodec FRAME_CODEC = new IotModbusFrameCodec(CUSTOM_FC); + + // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== + + private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; + private static final String DEVICE_NAME = "small"; + private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; + + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + + // ===================== 认证测试 ===================== + + /** + * 认证测试:发送自定义功能码 FC65 认证帧,验证认证成功响应 + */ + @Test + public void testAuth() throws Exception { + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + // 1. 构造并发送认证帧 + IotModbusFrame response = authenticate(socket); + + // 2. 验证响应 + log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]", + response.getSlaveId(), response.getFunctionCode(), response.getCustomData()); + JSONObject json = JSONUtil.parseObj(response.getCustomData()); + log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message")); + } finally { + socket.close(); + } + } + + // ===================== 轮询响应测试 ===================== + + /** + * 轮询响应测试:认证后等待网关下发 FC03 读请求,构造读响应帧发回 + */ + @Test + public void testPollingResponse() throws Exception { + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + // 1. 先认证 + IotModbusFrame authResponse = authenticate(socket); + log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); + + // 2. 等待网关下发读请求 + log.info("[testPollingResponse][等待网关下发读请求...]"); + IotModbusFrame readRequest = waitForRequest(socket); + log.info("[testPollingResponse][收到读请求: slaveId={}, FC={}, transactionId={}]", + readRequest.getSlaveId(), readRequest.getFunctionCode(), readRequest.getTransactionId()); + + // 3. 解析读请求中的起始地址和数量 + byte[] pdu = readRequest.getPdu(); + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); + + // 4. 构造读响应帧(模拟寄存器数据) + int[] registerValues = new int[quantity]; + for (int i = 0; i < quantity; i++) { + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... + } + byte[] responseData = buildReadResponse(readRequest.getTransactionId(), + readRequest.getSlaveId(), readRequest.getFunctionCode(), registerValues); + socket.write(Buffer.buffer(responseData)); + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); + + // 5. 等待一段时间让网关处理 + Thread.sleep(200000); + } finally { + socket.close(); + } + } + + // ===================== 属性设置测试 ===================== + + /** + * 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求 + *

                              + * 注意:需手动在平台触发 property.set + */ + @Test + public void testPropertySetWrite() throws Exception { + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + // 1. 先认证 + IotModbusFrame authResponse = authenticate(socket); + log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData()); + + // 2. 等待网关下发写请求(需手动在平台触发 property.set) + log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]"); + IotModbusFrame writeRequest = waitForRequest(socket); + log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, transactionId={}, pdu={}]", + writeRequest.getSlaveId(), writeRequest.getFunctionCode(), + writeRequest.getTransactionId(), bytesToHex(writeRequest.getPdu())); + } finally { + socket.close(); + } + } + + // ===================== 辅助方法 ===================== + + /** + * 建立 TCP 连接 + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + + /** + * 执行认证并返回响应帧 + */ + private IotModbusFrame authenticate(NetSocket socket) throws Exception { + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); + return sendAndReceive(socket, authFrame); + } + + /** + * 发送帧并等待响应(MODBUS_TCP 格式) + *

                              + * 使用两阶段 RecordParser 拆包:fixedSizeMode(6) 读 MBAP 头 → fixedSizeMode(length) 读 body + */ + private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { + CompletableFuture responseFuture = new CompletableFuture<>(); + // 创建 TCP 两阶段拆包 RecordParser + RecordParser parser = RecordParser.newFixed(6); + parser.handler(new TcpRecordParserHandler(parser, responseFuture)); + socket.handler(parser); + + // 发送请求 + log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length); + socket.write(Buffer.buffer(frameData)); + + // 等待响应 + return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + /** + * 等待接收网关下发的请求帧(不发送,只等待接收) + */ + private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { + CompletableFuture requestFuture = new CompletableFuture<>(); + RecordParser parser = RecordParser.newFixed(6); + parser.handler(new TcpRecordParserHandler(parser, requestFuture)); + socket.handler(parser); + + // 等待(超时 30 秒,因为轮询间隔可能比较长) + return requestFuture.get(30000, TimeUnit.MILLISECONDS); + } + + /** + * MODBUS_TCP 两阶段拆包 Handler + */ + private class TcpRecordParserHandler implements Handler { + + private final RecordParser parser; + private final CompletableFuture future; + private byte[] mbapHeader; + private boolean waitingForBody = false; + + TcpRecordParserHandler(RecordParser parser, CompletableFuture future) { + this.parser = parser; + this.future = future; + } + + @Override + public void handle(Buffer buffer) { + try { + if (waitingForBody) { + // Phase 2: 收到 body(unitId + PDU) + byte[] body = buffer.getBytes(); + byte[] fullFrame = new byte[mbapHeader.length + body.length]; + System.arraycopy(mbapHeader, 0, fullFrame, 0, mbapHeader.length); + System.arraycopy(body, 0, fullFrame, mbapHeader.length, body.length); + + IotModbusFrame frame = FRAME_CODEC.decodeResponse(fullFrame, IotModbusFrameFormatEnum.MODBUS_TCP); + future.complete(frame); + } else { + // Phase 1: 收到 MBAP 头 6 字节 + this.mbapHeader = buffer.getBytes(); + int length = ((mbapHeader[4] & 0xFF) << 8) | (mbapHeader[5] & 0xFF); + this.waitingForBody = true; + parser.fixedSizeMode(length); + } + } catch (Exception e) { + future.completeExceptionally(e); + } + } + } + + /** + * 构造认证帧(MODBUS_TCP 格式) + *

                              + * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}} + */ + private byte[] buildAuthFrame(String clientId, String username, String password) { + JSONObject params = new JSONObject(); + params.set("clientId", clientId); + params.set("username", username); + params.set("password", password); + JSONObject json = new JSONObject(); + json.set("method", "auth"); + json.set("params", params); + return FRAME_CODEC.encodeCustomFrame(SLAVE_ID, json.toString(), + IotModbusFrameFormatEnum.MODBUS_TCP, 1); + } + + /** + * 构造 FC03/FC01-04 读响应帧(MODBUS_TCP 格式) + *

                              + * 格式:[MBAP(6)] [UnitId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)] + */ + private byte[] buildReadResponse(int transactionId, int slaveId, int functionCode, int[] registerValues) { + int byteCount = registerValues.length * 2; + // PDU: FC(1) + ByteCount(1) + Data(N*2) + int pduLength = 1 + 1 + byteCount; + // 完整帧:MBAP(6) + UnitId(1) + PDU + int totalLength = 6 + 1 + pduLength; + ByteBuffer buf = ByteBuffer.allocate(totalLength).order(ByteOrder.BIG_ENDIAN); + // MBAP Header + buf.putShort((short) transactionId); // Transaction ID + buf.putShort((short) 0); // Protocol ID + buf.putShort((short) (1 + pduLength)); // Length (UnitId + PDU) + // UnitId + buf.put((byte) slaveId); + // PDU + buf.put((byte) functionCode); + buf.put((byte) byteCount); + for (int value : registerValues) { + buf.putShort((short) value); + } + return buf.array(); + } + + /** + * 字节数组转十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + if (bytes == null) { + return "null"; + } + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X ", b)); + } + return sb.toString().trim(); + } + +} From 346ae3ff48d3ac31b25cad6dbbe03457f90cbfa8 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 8 Feb 2026 01:41:30 +0800 Subject: [PATCH 34/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9Amodbus-?= =?UTF-8?q?tcp-slave=20=E4=BC=98=E5=8C=96=E5=AE=83=E7=9A=84=20code?= =?UTF-8?q?=E3=80=81decode=20=E9=80=BB=E8=BE=91=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tcpslave/IotModbusTcpSlaveProtocol.java | 144 ++++----- ...actory.java => IotModbusFrameDecoder.java} | 297 ++++++++++++------ ...eCodec.java => IotModbusFrameEncoder.java} | 155 +-------- ...esponseParser.java => IotModbusUtils.java} | 53 +++- .../IotModbusTcpSlaveDownstreamHandler.java | 8 +- .../IotModbusTcpSlaveUpstreamHandler.java | 266 ++++++++-------- .../IotModbusTcpSlavePollScheduler.java | 6 +- ...odbusTcpSlaveModbusRtuIntegrationTest.java | 87 +++-- ...odbusTcpSlaveModbusTcpIntegrationTest.java | 124 +++----- 9 files changed, 525 insertions(+), 615 deletions(-) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/{IotModbusRecordParserFactory.java => IotModbusFrameDecoder.java} (52%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/{IotModbusFrameCodec.java => IotModbusFrameEncoder.java} (54%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/{IotModbusResponseParser.java => IotModbusUtils.java} (61%) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java index a80471b9e8..ca317ec3fe 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java @@ -4,7 +4,6 @@ import cn.hutool.core.lang.Assert; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; @@ -12,9 +11,8 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusRecordParserFactory; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream.IotModbusTcpSlaveUpstreamHandler; @@ -23,6 +21,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotM import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; import io.vertx.core.net.NetServer; @@ -33,19 +32,15 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -// TODO @AI:不用主动上报! +// DONE @AI:不用主动上报! /** * IoT 网关 Modbus TCP Slave 协议 *

                              * 作为 TCP Server 接收设备主动连接: * 1. 设备通过自定义功能码(FC 65)发送认证请求 - * 2. 认证成功后,根据设备配置的 mode 决定工作模式: - * - mode=1(云端轮询):网关主动发送 Modbus 读请求,设备响应 - * - mode=2(主动上报):设备主动上报数据,网关透传 + * 2. 认证成功后,网关主动发送 Modbus 读请求,设备响应(云端轮询模式) * * @author 芋道源码 */ @@ -85,20 +80,16 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { */ private Long requestCleanupTimerId; - /** - * 未认证连接的帧格式缓存:socket → 检测到的帧格式 - */ - private final Map pendingFrameFormats = new ConcurrentHashMap<>(); - // ========== 各组件 ========== + // TODO @芋艿:稍后排序下,有点小乱; private final IotModbusTcpSlaveConfig slaveConfig; - private final IotModbusFrameCodec frameCodec; + private final IotModbusFrameDecoder frameDecoder; + private final IotModbusFrameEncoder frameEncoder; private final IotModbusTcpSlaveConnectionManager connectionManager; private final IotModbusTcpSlaveConfigCacheService configCacheService; private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; private final IotModbusTcpSlaveUpstreamHandler upstreamHandler; - private final IotModbusTcpSlaveDownstreamHandler downstreamHandler; private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; private final IotModbusTcpSlavePollScheduler pollScheduler; @@ -118,33 +109,27 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { this.pendingRequestManager = new IotModbusTcpSlavePendingRequestManager(); // 初始化帧编解码器 - this.frameCodec = new IotModbusFrameCodec(slaveConfig.getCustomFunctionCode()); + this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode()); + this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode()); + + // 初始化轮询调度器 + this.pollScheduler = new IotModbusTcpSlavePollScheduler( + vertx, connectionManager, frameEncoder, pendingRequestManager, + slaveConfig.getRequestTimeout()); // 初始化 Handler IotModbusDataConverter dataConverter = new IotModbusDataConverter(); IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); + IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler( - deviceApi, messageService, dataConverter, frameCodec, - connectionManager, configCacheService, pendingRequestManager, serverId); - this.downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( - connectionManager, configCacheService, dataConverter, frameCodec); - - // 初始化轮询调度器 - this.pollScheduler = new IotModbusTcpSlavePollScheduler( - vertx, connectionManager, frameCodec, pendingRequestManager, - slaveConfig.getRequestTimeout()); - - // 设置认证成功回调:启动轮询 - // TODO @AI:感觉直接去调用,不用注册回调了(更简洁) - this.upstreamHandler.setOnAuthSuccess((deviceId, config) -> { - if (config.getMode() != null - && config.getMode().equals(IotModbusModeEnum.POLLING.getMode())) { - pollScheduler.updatePolling(config); - } - }); + deviceApi, messageService, dataConverter, frameEncoder, + connectionManager, configCacheService, pendingRequestManager, + pollScheduler, deviceService, serverId); // 初始化下行消息订阅者 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotModbusTcpSlaveDownstreamHandler downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( + connectionManager, configCacheService, dataConverter, frameEncoder); this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber( this, downstreamHandler, messageBus); } @@ -168,6 +153,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { try { // 1.1 首次加载配置 + // TODO @AI:可能首次不用加载;你在想想; refreshConfig(); // 1.2 启动配置刷新定时器 int refreshInterval = slaveConfig.getConfigRefreshInterval(); @@ -190,10 +176,21 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT Modbus TCP Slave 协议 {} 启动失败]", getId(), e); + if (configRefreshTimerId != null) { + vertx.cancelTimer(configRefreshTimerId); + configRefreshTimerId = null; + } + if (requestCleanupTimerId != null) { + vertx.cancelTimer(requestCleanupTimerId); + requestCleanupTimerId = null; + } + connectionManager.closeAll(); + if (netServer != null) { + netServer.close(); + } if (vertx != null) { vertx.close(); } - // TODO @AI:其它相关的 close; throw e; } } @@ -252,20 +249,19 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { * 启动 TCP Server */ private void startTcpServer() { - // TODO @AI:host 一定要设置么? // 1. 创建 TCP Server NetServerOptions options = new NetServerOptions() - .setPort(properties.getPort()) - .setHost("0.0.0.0"); + .setPort(properties.getPort()); netServer = vertx.createNetServer(options); // 2. 设置连接处理器 netServer.connectHandler(this::handleConnection); - // TODO @AI:是不是 sync 就好,不用 onSuccess/onFailure 了?感觉更简洁。失败,肯定就要抛出异常,结束初始化了! - netServer.listen() - .onSuccess(server -> log.info("[startTcpServer][TCP Server 启动成功, port={}]", - server.actualPort())) - .onFailure(e -> log.error("[startTcpServer][TCP Server 启动失败]", e)); + try { + netServer.listen().toCompletionStage().toCompletableFuture().get(); + log.info("[startTcpServer][TCP Server 启动成功, port={}]", properties.getPort()); + } catch (Exception e) { + throw new RuntimeException("[startTcpServer][TCP Server 启动失败]", e); + } } /** @@ -274,58 +270,24 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { private void handleConnection(NetSocket socket) { log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress()); - // 1.1 创建带帧格式检测的 RecordParser - // TODO @AI:看看怎么从这个类里面,拿出去;让这个类的职责更单一; - RecordParser parser = IotModbusRecordParserFactory.create( - slaveConfig.getCustomFunctionCode(), - // 完整帧回调 - // TODO @AI:感觉搞个独立的类,稍微好点?! - frameBuffer -> { - byte[] frameBytes = frameBuffer.getBytes(); - // 获取该连接的帧格式 - ConnectionInfo connInfo = connectionManager.getConnectionInfo(socket); - IotModbusFrameFormatEnum frameFormat = connInfo != null ? connInfo.getFrameFormat() : null; - if (frameFormat == null) { - // 未认证的连接,使用首帧检测到的帧格式 - frameFormat = pendingFrameFormats.get(socket); - } - if (frameFormat == null) { - log.warn("[handleConnection][帧格式未检测到, remoteAddress={}]", socket.remoteAddress()); - return; - } - - // 解码帧 - IotModbusFrame frame = frameCodec.decodeResponse(frameBytes, frameFormat); - // 交给 UpstreamHandler 处理 - upstreamHandler.handleFrame(socket, frame, frameFormat); - }, - // 帧格式检测回调:保存到未认证缓存 - detectedFormat -> { - // TODO @AI:是不是不用缓存,每次都探测;因为一般 auth 首包后,基本也没探测的诉求了! - pendingFrameFormats.put(socket, detectedFormat); - // 如果连接已注册(不太可能在检测阶段),也更新 - // TODO @AI:是否非必须?! - connectionManager.setFrameFormat(socket, detectedFormat); - log.debug("[handleConnection][帧格式检测: {}, remoteAddress={}]", - detectedFormat, socket.remoteAddress()); - } - ); - // 1.2 设置数据处理器 - socket.handler(parser); + // 1. 创建 RecordParser 并设置为数据处理器 + RecordParser recordParser = frameDecoder.createRecordParser((frame, frameFormat) -> { + // 【重要】帧处理分发,即消息处理 + upstreamHandler.handleFrame(socket, frame, frameFormat); + }); + socket.handler(recordParser); // 2.1 连接关闭处理 socket.closeHandler(v -> { - pendingFrameFormats.remove(socket); ConnectionInfo info = connectionManager.removeConnection(socket); - // TODO @AI:if return 简化下; - if (info != null && info.getDeviceId() != null) { - pollScheduler.stopPolling(info.getDeviceId()); - pendingRequestManager.removeDevice(info.getDeviceId()); - log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", - info.getDeviceId(), socket.remoteAddress()); - } else { + if (info == null || info.getDeviceId() == null) { log.info("[handleConnection][未认证连接关闭, remoteAddress={}]", socket.remoteAddress()); + return; } + pollScheduler.stopPolling(info.getDeviceId()); + pendingRequestManager.removeDevice(info.getDeviceId()); + log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", + info.getDeviceId(), socket.remoteAddress()); }); // 2.2 异常处理 socket.exceptionHandler(e -> { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusRecordParserFactory.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java similarity index 52% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusRecordParserFactory.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java index 07d98d3d55..8ea473738f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusRecordParserFactory.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java @@ -4,116 +4,205 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.function.Consumer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.function.BiConsumer; -// TODO @AI:看看是不是不要搞成 factory,而是直接 new;(可以一起讨论下) /** - * IoT Modbus RecordParser 工厂 + * IoT Modbus 帧解码器:集成 TCP 拆包 + 帧格式探测 + 帧解码,一条龙完成从 TCP 字节流到 IotModbusFrame 的转换。 *

                              - * 创建带自动帧格式检测的 RecordParser: + * 流程: * 1. 首帧检测:读前 6 字节,判断 MODBUS_TCP(ProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU - * 2. 检测后自动切换到对应的拆包模式 + * 2. 检测后切换到对应的拆包 Handler,并将首包 6 字节通过 handleFirstBytes() 交给新 Handler 处理 + * 3. 拆包完成后解码为 IotModbusFrame,通过回调返回 * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动) * - MODBUS_RTU:功能码驱动的状态机 * * @author 芋道源码 */ @Slf4j -public class IotModbusRecordParserFactory { +public class IotModbusFrameDecoder { + + private final int customFunctionCode; + + public IotModbusFrameDecoder(int customFunctionCode) { + this.customFunctionCode = customFunctionCode; + } /** * 创建带自动帧格式检测的 RecordParser * - * @param customFunctionCode 自定义功能码 - * @param frameHandler 完整帧回调 - * @param onFormatDetected 帧格式检测回调 + * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式) * @return RecordParser 实例 */ - public static RecordParser create(int customFunctionCode, - Handler frameHandler, - Consumer onFormatDetected) { - // 先创建一个 RecordParser,使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测 - // TODO @AI:最小需要 6 个字节么?有可能更小的情况下,就探测出来?! + public RecordParser createRecordParser(BiConsumer frameHandler) { + // 先创建一个 RecordParser:使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测 RecordParser parser = RecordParser.newFixed(6); - parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler, onFormatDetected)); + parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler)); return parser; } + // ==================== 帧解码 ==================== + /** - * 帧格式检测阶段 Handler + * 解码响应帧(拆包后的完整帧 byte[]) + * + * @param data 完整帧字节数组 + * @param format 帧格式 + * @return 解码后的 IotModbusFrame + */ + private IotModbusFrame decodeResponse(byte[] data, IotModbusFrameFormatEnum format) { + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { + return decodeTcpResponse(data); + } else { + return decodeRtuResponse(data); + } + } + + /** + * 解码 MODBUS_TCP 响应 + * 格式:[TransactionId(2)] [ProtocolId(2)] [Length(2)] [UnitId(1)] [FC(1)] [Data...] + */ + private IotModbusFrame decodeTcpResponse(byte[] data) { + if (data.length < 8) { + log.warn("[decodeTcpResponse][数据长度不足: {}]", data.length); + return null; + } + ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN); + int transactionId = buf.getShort() & 0xFFFF; + buf.getShort(); // protocolId:固定 0x0000,Modbus 协议标识 + buf.getShort(); // length:后续字节数(UnitId + PDU),拆包阶段已使用 + int slaveId = buf.get() & 0xFF; + int functionCode = buf.get() & 0xFF; + // 提取 PDU 数据(从 functionCode 之后到末尾) + byte[] pdu = new byte[data.length - 8]; + System.arraycopy(data, 8, pdu, 0, pdu.length); + + return buildFrame(slaveId, functionCode, pdu, transactionId); + } + + /** + * 解码 MODBUS_RTU 响应 + * 格式:[SlaveId(1)] [FC(1)] [Data...] [CRC(2)] + */ + private IotModbusFrame decodeRtuResponse(byte[] data) { + if (data.length < 4) { + log.warn("[decodeRtuResponse][数据长度不足: {}]", data.length); + return null; + } + // 校验 CRC + if (!IotModbusUtils.verifyCrc16(data)) { + log.warn("[decodeRtuResponse][CRC 校验失败]"); + return null; + } + int slaveId = data[0] & 0xFF; + int functionCode = data[1] & 0xFF; + // PDU 数据(不含 slaveId、functionCode、CRC) + byte[] pdu = new byte[data.length - 4]; + System.arraycopy(data, 2, pdu, 0, pdu.length); + + return buildFrame(slaveId, functionCode, pdu, null); + } + + /** + * 构建 IotModbusFrame + */ + private IotModbusFrame buildFrame(int slaveId, int functionCode, byte[] pdu, Integer transactionId) { + IotModbusFrame frame = new IotModbusFrame() + .setSlaveId(slaveId) + .setFunctionCode(functionCode) + .setPdu(pdu) + .setTransactionId(transactionId); + // 异常响应 + // TODO @AI:0x80 看看是不是要枚举; + if ((functionCode & 0x80) != 0) { + frame.setException(true); + // TODO @AI:0x7f 看看是不是要枚举; + frame.setFunctionCode(functionCode & 0x7F); + if (pdu.length >= 1) { + frame.setExceptionCode(pdu[0] & 0xFF); + } + return frame; + } + // 自定义功能码 + if (functionCode == customFunctionCode) { + // data 区格式:[byteCount(1)] [JSON data(N)] + if (pdu.length >= 1) { + int byteCount = pdu[0] & 0xFF; + if (pdu.length >= 1 + byteCount) { + frame.setCustomData(new String(pdu, 1, byteCount, StandardCharsets.UTF_8)); + } + } + } + return frame; + } + + // ==================== 拆包 Handler ==================== + + /** + * 帧格式检测阶段 Handler(仅处理首包,探测后切换到对应的拆包 Handler) */ @SuppressWarnings("ClassCanBeRecord") - private static class DetectPhaseHandler implements Handler { + @RequiredArgsConstructor + private class DetectPhaseHandler implements Handler { private final RecordParser parser; private final int customFunctionCode; - private final Handler frameHandler; - private final Consumer onFormatDetected; - - // TODO @AI:简化构造方法,使用 lombok; - DetectPhaseHandler(RecordParser parser, int customFunctionCode, - Handler frameHandler, - Consumer onFormatDetected) { - this.parser = parser; - this.customFunctionCode = customFunctionCode; - this.frameHandler = frameHandler; - this.onFormatDetected = onFormatDetected; - } + private final BiConsumer frameHandler; @Override public void handle(Buffer buffer) { - byte[] header = buffer.getBytes(); - // 检测:byte[2]==0x00 && byte[3]==0x00 && 1<=length<=253 - int protocolId = ((header[2] & 0xFF) << 8) | (header[3] & 0xFF); - int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF); + // 检测帧格式:protocolId==0x0000 且 length 合法 → MODBUS_TCP,否则 → MODBUS_RTU + byte[] bytes = buffer.getBytes(); + int protocolId = ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); + int length = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF); + // 分别处理 MODBUS_TCP、MODBUS_RTU 两种情况 if (protocolId == 0x0000 && length >= 1 && length <= 253) { - // MODBUS_TCP + // MODBUS_TCP:切换到 TCP 拆包 Handler log.debug("[DetectPhaseHandler][检测到 MODBUS_TCP 帧格式]"); - onFormatDetected.accept(IotModbusFrameFormatEnum.MODBUS_TCP); - // 切换到 TCP 拆包模式,处理当前首帧 TcpFrameHandler tcpHandler = new TcpFrameHandler(parser, frameHandler); parser.handler(tcpHandler); - // 当前 header 是 MBAP 的前 6 字节,需要继续读 length 字节 - tcpHandler.handleMbapHeader(header, length); + // 当前 bytes 就是 MBAP 的前 6 字节,直接交给 tcpHandler 处理 + tcpHandler.handleFirstBytes(bytes); } else { - // MODBUS_RTU + // MODBUS_RTU:切换到 RTU 拆包 Handler log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]"); - onFormatDetected.accept(IotModbusFrameFormatEnum.MODBUS_RTU); - // 切换到 RTU 拆包模式,处理当前首帧 - RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, customFunctionCode, frameHandler); + RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, frameHandler, customFunctionCode); parser.handler(rtuHandler); - // 当前 header 包含前 6 字节(slaveId + FC + 部分数据),需要拼接处理 - rtuHandler.handleInitialBytes(header); + // 当前 bytes 包含前 6 字节(slaveId + FC + 部分数据),交给 rtuHandler 处理 + rtuHandler.handleFirstBytes(bytes); } } } /** * MODBUS_TCP 拆包 Handler(两阶段 RecordParser) + *

                              * Phase 1: fixedSizeMode(6) → 读 MBAP 前 6 字节,提取 length * Phase 2: fixedSizeMode(length) → 读 unitId + PDU */ - private static class TcpFrameHandler implements Handler { + @RequiredArgsConstructor + private class TcpFrameHandler implements Handler { private final RecordParser parser; - private final Handler frameHandler; + private final BiConsumer frameHandler; + private byte[] mbapHeader; private boolean waitingForBody = false; - // TODO @AI:lombok - TcpFrameHandler(RecordParser parser, Handler frameHandler) { - this.parser = parser; - this.frameHandler = frameHandler; - } - /** - * 处理首帧的 MBAP 头 + * 处理探测阶段传来的首帧 6 字节(即 MBAP 头) + * + * @param bytes 探测阶段消费的 6 字节 */ - void handleMbapHeader(byte[] header, int length) { - this.mbapHeader = header; + void handleFirstBytes(byte[] bytes) { + int length = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF); + this.mbapHeader = bytes; this.waitingForBody = true; parser.fixedSizeMode(length); } @@ -124,10 +213,14 @@ public class IotModbusRecordParserFactory { // Phase 2: 收到 body(unitId + PDU) byte[] body = buffer.getBytes(); // 拼接完整帧:MBAP(6) + body - Buffer frame = Buffer.buffer(mbapHeader.length + body.length); - frame.appendBytes(mbapHeader); - frame.appendBytes(body); - frameHandler.handle(frame); + byte[] fullFrame = new byte[mbapHeader.length + body.length]; + System.arraycopy(mbapHeader, 0, fullFrame, 0, mbapHeader.length); + System.arraycopy(body, 0, fullFrame, mbapHeader.length, body.length); + // 解码并回调 + IotModbusFrame frame = decodeResponse(fullFrame, IotModbusFrameFormatEnum.MODBUS_TCP); + if (frame != null) { + frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_TCP); + } // 切回 Phase 1 waitingForBody = false; mbapHeader = null; @@ -159,7 +252,8 @@ public class IotModbusRecordParserFactory { * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2) * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2) */ - private static class RtuFrameHandler implements Handler { + @RequiredArgsConstructor + private class RtuFrameHandler implements Handler { private static final int STATE_HEADER = 0; private static final int STATE_EXCEPTION_BODY = 1; @@ -168,50 +262,41 @@ public class IotModbusRecordParserFactory { private static final int STATE_WRITE_BODY = 4; private final RecordParser parser; + private final BiConsumer frameHandler; private final int customFunctionCode; - private final Handler frameHandler; private int state = STATE_HEADER; private byte slaveId; private byte functionCode; private byte byteCount; - - // TODO @AI:lombok - RtuFrameHandler(RecordParser parser, int customFunctionCode, Handler frameHandler) { - this.parser = parser; - this.customFunctionCode = customFunctionCode; - this.frameHandler = frameHandler; - } + private Buffer pendingData; + private int expectedDataLen; /** - * 处理首帧检测阶段传来的初始 6 字节 - * 由于 RTU 首帧跳过了格式检测,我们需要拼接处理 + * 处理探测阶段传来的首帧 6 字节 + *

                              + * 由于 RTU 首帧被探测阶段消费了 6 字节,这里需要从中提取 slaveId + FC 并根据 FC 处理剩余数据 + * + * @param bytes 探测阶段消费的 6 字节:[slaveId][FC][...4 bytes...] */ - void handleInitialBytes(byte[] initialBytes) { - // initialBytes 包含 6 字节:[slaveId][FC][...4 bytes...] - this.slaveId = initialBytes[0]; - this.functionCode = initialBytes[1]; + void handleFirstBytes(byte[] bytes) { + this.slaveId = bytes[0]; + this.functionCode = bytes[1]; int fc = functionCode & 0xFF; - - // 根据功能码,确定还需要多少字节 if ((fc & 0x80) != 0) { - // 异常响应:还需要 exceptionCode(1) + CRC(2) = 3 字节 - // 我们已经有 4 字节剩余(initialBytes[2..5]),足够 - // 拼接完整帧并交付 - // 完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 + // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节 + // 已有 6 字节(多 1 字节),取前 5 字节组装 Buffer frame = Buffer.buffer(5); frame.appendByte(slaveId); frame.appendByte(functionCode); - frame.appendBytes(initialBytes, 2, 3); // exceptionCode + CRC - frameHandler.handle(frame); - // 剩余 1 字节需要留给下一帧,但 RecordParser 不支持回推 - // 简化处理:重置状态,开始读下一帧 + frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC + emitFrame(frame); resetToHeader(); } else if (isReadResponse(fc) || fc == customFunctionCode) { - // 读响应或自定义 FC:initialBytes[2] = byteCount - this.byteCount = initialBytes[2]; + // 读响应或自定义 FC:bytes[2] = byteCount + this.byteCount = bytes[2]; int bc = byteCount & 0xFF; - // 已有数据:initialBytes[3..5] = 3 字节 + // 已有数据:bytes[3..5] = 3 字节 // 还需:byteCount + CRC(2) - 3 字节已有 int remaining = bc + 2 - 3; if (remaining <= 0) { @@ -221,36 +306,30 @@ public class IotModbusRecordParserFactory { frame.appendByte(slaveId); frame.appendByte(functionCode); frame.appendByte(byteCount); - frame.appendBytes(initialBytes, 3, bc + 2); // data + CRC - frameHandler.handle(frame); + frame.appendBytes(bytes, 3, bc + 2); // data + CRC + emitFrame(frame); resetToHeader(); } else { // 需要继续读 state = STATE_READ_DATA; - // 保存已有数据片段 - parser.fixedSizeMode(remaining); - // 在 handle() 中需要拼接 initialBytes[3..5] + 新读取的数据 - // 为了简化,我们用一个 Buffer 暂存 this.pendingData = Buffer.buffer(); - this.pendingData.appendBytes(initialBytes, 3, 3); + this.pendingData.appendBytes(bytes, 3, 3); // 暂存已有的 3 字节 this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC + parser.fixedSizeMode(remaining); } } else if (isWriteResponse(fc)) { - // 写响应:FC05/06/15/16,总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 + // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节 // 已有 6 字节,还需 2 字节 state = STATE_WRITE_BODY; this.pendingData = Buffer.buffer(); - this.pendingData.appendBytes(initialBytes, 2, 4); // 4 bytes already - parser.fixedSizeMode(2); // need 2 more bytes (CRC) + this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节 + parser.fixedSizeMode(2); // 还需 2 字节(CRC) } else { log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc)); resetToHeader(); } } - private Buffer pendingData; - private int expectedDataLen; - @Override public void handle(Buffer buffer) { switch (state) { @@ -279,7 +358,6 @@ public class IotModbusRecordParserFactory { this.slaveId = header[0]; this.functionCode = header[1]; int fc = functionCode & 0xFF; - if ((fc & 0x80) != 0) { // 异常响应 state = STATE_EXCEPTION_BODY; @@ -305,7 +383,7 @@ public class IotModbusRecordParserFactory { frame.appendByte(slaveId); frame.appendByte(functionCode); frame.appendBuffer(buffer); - frameHandler.handle(frame); + emitFrame(frame); resetToHeader(); } @@ -327,7 +405,7 @@ public class IotModbusRecordParserFactory { frame.appendByte(functionCode); frame.appendByte(byteCount); frame.appendBuffer(pendingData); - frameHandler.handle(frame); + emitFrame(frame); resetToHeader(); } // 否则继续等待(不应该发生,因为我们精确设置了 fixedSizeMode) @@ -340,16 +418,27 @@ public class IotModbusRecordParserFactory { frame.appendByte(slaveId); frame.appendByte(functionCode); frame.appendBuffer(pendingData); - frameHandler.handle(frame); + emitFrame(frame); resetToHeader(); } + /** + * 发射完整帧:解码并回调 + */ + private void emitFrame(Buffer frameBuffer) { + IotModbusFrame frame = decodeResponse(frameBuffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU); + if (frame != null) { + frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_RTU); + } + } + private void resetToHeader() { state = STATE_HEADER; pendingData = null; parser.fixedSizeMode(2); // slaveId + FC } + // TODO @AI:可以抽到 IotModbusUtils 里? private boolean isReadResponse(int fc) { return fc >= 1 && fc <= 4; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java similarity index 54% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameCodec.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java index 8d65b30bc1..0790ab3b36 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java @@ -1,128 +1,24 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; /** - * IoT Modbus 帧编解码器 + * IoT Modbus 帧编码器 *

                              - * 纯 Modbus 协议编解码,不处理 TCP 粘包(由 RecordParser 处理)。 - * 支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式,以及自定义功能码扩展。 + * 负责将 Modbus 请求/响应编码为字节数组,支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式。 * * @author 芋道源码 */ +@RequiredArgsConstructor @Slf4j -public class IotModbusFrameCodec { +public class IotModbusFrameEncoder { private final int customFunctionCode; - public IotModbusFrameCodec(int customFunctionCode) { - this.customFunctionCode = customFunctionCode; - } - - // ==================== 解码 ==================== - - /** - * 解码响应帧(拆包后的完整帧 byte[]) - * - * @param data 完整帧字节数组 - * @param format 帧格式 - * @return 解码后的 IotModbusFrame - */ - public IotModbusFrame decodeResponse(byte[] data, IotModbusFrameFormatEnum format) { - if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { - return decodeTcpResponse(data); - } else { - return decodeRtuResponse(data); - } - } - - /** - * 解码 MODBUS_TCP 响应 - * 格式:[TransactionId(2)] [ProtocolId(2)] [Length(2)] [UnitId(1)] [FC(1)] [Data...] - */ - private IotModbusFrame decodeTcpResponse(byte[] data) { - if (data.length < 8) { - log.warn("[decodeTcpResponse][数据长度不足: {}]", data.length); - return null; - } - ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN); - int transactionId = buf.getShort() & 0xFFFF; - buf.getShort(); // protocolId(跳过)// TODO @AI:跳过原因,最好写下; - buf.getShort(); // length(跳过)// TODO @AI:跳过原因,最好写下; - int slaveId = buf.get() & 0xFF; - int functionCode = buf.get() & 0xFF; - // 提取 PDU 数据(从 functionCode 之后到末尾) - byte[] pdu = new byte[data.length - 8]; - System.arraycopy(data, 8, pdu, 0, pdu.length); - - // 构建 IotModbusFrame - return buildFrame(slaveId, functionCode, pdu, transactionId); - } - - /** - * 解码 MODBUS_RTU 响应 - * 格式:[SlaveId(1)] [FC(1)] [Data...] [CRC(2)] - */ - private IotModbusFrame decodeRtuResponse(byte[] data) { - if (data.length < 4) { - log.warn("[decodeRtuResponse][数据长度不足: {}]", data.length); - return null; - } - // 校验 CRC - if (!verifyCrc16(data)) { - log.warn("[decodeRtuResponse][CRC 校验失败]"); - return null; - } - int slaveId = data[0] & 0xFF; - int functionCode = data[1] & 0xFF; - // PDU 数据(不含 slaveId、functionCode、CRC) - byte[] pdu = new byte[data.length - 4]; - System.arraycopy(data, 2, pdu, 0, pdu.length); - - // 构建 IotModbusFrame - return buildFrame(slaveId, functionCode, pdu, null); - } - - /** - * 构建 IotModbusFrame - */ - private IotModbusFrame buildFrame(int slaveId, int functionCode, byte[] pdu, Integer transactionId) { - IotModbusFrame frame = new IotModbusFrame() - .setSlaveId(slaveId) - .setFunctionCode(functionCode) - .setPdu(pdu) - .setTransactionId(transactionId); - - // 异常响应 - // TODO @AI:0x80 看看是不是要枚举; - if ((functionCode & 0x80) != 0) { - frame.setException(true); - // TODO @AI:0x7f 看看是不是要枚举; - frame.setFunctionCode(functionCode & 0x7F); - if (pdu.length >= 1) { - frame.setExceptionCode(pdu[0] & 0xFF); - } - return frame; - } - - // 自定义功能码 - if (functionCode == customFunctionCode) { - // data 区格式:[byteCount(1)] [JSON data(N)] - if (pdu.length >= 1) { - int byteCount = pdu[0] & 0xFF; - if (pdu.length >= 1 + byteCount) { - frame.setCustomData(new String(pdu, 1, byteCount, StandardCharsets.UTF_8)); - } - } - } - return frame; - } - // ==================== 编码 ==================== /** @@ -269,49 +165,10 @@ public class IotModbusFrameCodec { frame[0] = (byte) slaveId; System.arraycopy(pdu, 0, frame, 1, pdu.length); // 计算并追加 CRC16 - int crc = calculateCrc16(frame, frame.length - 2); + int crc = IotModbusUtils.calculateCrc16(frame, frame.length - 2); frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High return frame; } - // ==================== CRC16 工具 ==================== - - // TODO @AI:hutool 等,有没工具类可以用 - /** - * 计算 CRC-16/MODBUS - * - * @param data 数据 - * @param length 计算长度 - * @return CRC16 值 - */ - public static int calculateCrc16(byte[] data, int length) { - int crc = 0xFFFF; - for (int i = 0; i < length; i++) { - crc ^= (data[i] & 0xFF); - for (int j = 0; j < 8; j++) { - if ((crc & 0x0001) != 0) { - crc >>= 1; - crc ^= 0xA001; - } else { - crc >>= 1; - } - } - } - return crc; - } - - // TODO @AI:hutool 等,有没工具类可以用 - /** - * 校验 CRC16 - */ - private boolean verifyCrc16(byte[] data) { - if (data.length < 3) { - return false; - } - int computed = calculateCrc16(data, data.length - 2); - int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8); - return computed == received; - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusResponseParser.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusUtils.java similarity index 61% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusResponseParser.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusUtils.java index 6fa363db0b..f93f55cf9a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusResponseParser.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusUtils.java @@ -3,14 +3,61 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; import lombok.extern.slf4j.Slf4j; /** - * IoT Modbus 响应值提取器 + * IoT Modbus 工具类 *

                              - * 从解码后的 IotModbusFrame 中提取寄存器值,用于后续的点位翻译。 + * 提供: + * 1. CRC-16/MODBUS 计算和校验 + * 2. 从解码后的 IotModbusFrame 中提取寄存器值(用于后续的点位翻译) * * @author 芋道源码 */ @Slf4j -public class IotModbusResponseParser { +public class IotModbusUtils { + + // TODO @AI:可以把 1、2、3、4、5 这些 fucntion code 在这里枚举下。 + // TODO @AI:一些枚举 0x80 这些可以这里枚举; + + // ==================== CRC16 工具 ==================== + + /** + * 计算 CRC-16/MODBUS + * + * @param data 数据 + * @param length 计算长度 + * @return CRC16 值 + */ + public static int calculateCrc16(byte[] data, int length) { + int crc = 0xFFFF; + for (int i = 0; i < length; i++) { + crc ^= (data[i] & 0xFF); + for (int j = 0; j < 8; j++) { + if ((crc & 0x0001) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; + } + + /** + * 校验 CRC16 + * + * @param data 包含 CRC 的完整数据 + * @return 校验是否通过 + */ + public static boolean verifyCrc16(byte[] data) { + if (data.length < 3) { + return false; + } + int computed = calculateCrc16(data, data.length - 2); + int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8); + return computed == received; + } + + // ==================== 响应值提取 ==================== /** * 从帧中提取寄存器值(FC01-04 读响应) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java index 76e904ee7b..5a47a09c16 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; @@ -35,7 +35,7 @@ public class IotModbusTcpSlaveDownstreamHandler { private final IotModbusTcpSlaveConnectionManager connectionManager; private final IotModbusTcpSlaveConfigCacheService configCacheService; private final IotModbusDataConverter dataConverter; - private final IotModbusFrameCodec frameCodec; + private final IotModbusFrameEncoder frameEncoder; /** * TCP 事务 ID 自增器 @@ -117,11 +117,11 @@ public class IotModbusTcpSlaveDownstreamHandler { } if (rawValues.length == 1 && fcEnum.getWriteSingleCode() != null) { // 单个值:使用单写功能码(FC05/FC06) - data = frameCodec.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(), + data = frameEncoder.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(), point.getRegisterAddress(), rawValues[0], frameFormat, transactionId); } else if (fcEnum.getWriteMultipleCode() != null) { // 多个值:使用多写功能码(FC15/FC16) - data = frameCodec.encodeWriteMultipleRegistersRequest(slaveId, + data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId, point.getRegisterAddress(), rawValues, frameFormat, transactionId); } else { log.warn("[writeProperty][点位 {} 不支持写操作]", point.getIdentifier()); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java index 4d91b636bf..15ae18f996 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java @@ -1,11 +1,13 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; @@ -19,69 +21,76 @@ import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusResponseParser; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.net.NetSocket; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import java.util.Map; -import java.util.function.BiConsumer; -// TODO @AI:逻辑有点多,看看是不是分区域! +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; + +// DONE @AI:逻辑有点多,看看是不是分区域! => 已按区域划分:认证 / 轮询响应 /** * IoT Modbus TCP Slave 上行数据处理器 *

                              * 处理: * 1. 自定义 FC 认证 * 2. 轮询响应(mode=1)→ 点位翻译 → thing.property.post - * 3. 主动上报(mode=2)→ 透传 property.report TODO @AI:这种模式,应该不用支持;因为主动上报,都走标准的 tcp 即可; + * // DONE @AI:不用主动上报;主动上报走标准 tcp 即可 * * @author 芋道源码 */ @Slf4j public class IotModbusTcpSlaveUpstreamHandler { + private static final String METHOD_AUTH = "auth"; + private final IotDeviceCommonApi deviceApi; private final IotDeviceMessageService messageService; private final IotModbusDataConverter dataConverter; - private final IotModbusFrameCodec frameCodec; + private final IotModbusFrameEncoder frameEncoder; private final IotModbusTcpSlaveConnectionManager connectionManager; private final IotModbusTcpSlaveConfigCacheService configCacheService; private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; + private final IotModbusTcpSlavePollScheduler pollScheduler; + private final IotDeviceService deviceService; private final String serverId; - /** - * 认证成功回调:(deviceId, config) → 启动轮询等 - */ - @Setter - private BiConsumer onAuthSuccess; - public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi, IotDeviceMessageService messageService, IotModbusDataConverter dataConverter, - IotModbusFrameCodec frameCodec, + IotModbusFrameEncoder frameEncoder, IotModbusTcpSlaveConnectionManager connectionManager, IotModbusTcpSlaveConfigCacheService configCacheService, IotModbusTcpSlavePendingRequestManager pendingRequestManager, + IotModbusTcpSlavePollScheduler pollScheduler, + IotDeviceService deviceService, String serverId) { this.deviceApi = deviceApi; this.messageService = messageService; this.dataConverter = dataConverter; - this.frameCodec = frameCodec; + this.frameEncoder = frameEncoder; this.connectionManager = connectionManager; this.configCacheService = configCacheService; this.pendingRequestManager = pendingRequestManager; + this.pollScheduler = pollScheduler; + this.deviceService = deviceService; this.serverId = serverId; } + // ========== 帧处理入口 ========== + /** * 处理帧 */ @@ -89,159 +98,158 @@ public class IotModbusTcpSlaveUpstreamHandler { if (frame == null) { return; } - // 1.1 自定义功能码(认证等扩展) - if (StrUtil.isNotEmpty(frame.getCustomData())) { - handleCustomFrame(socket, frame, frameFormat); - return; - } - // 1.2 异常响应 + // 1. 异常响应 if (frame.isException()) { - // TODO @AI:这种需要返回一个结果给 modbus client? log.warn("[handleFrame][设备异常响应, slaveId={}, FC={}, exceptionCode={}]", frame.getSlaveId(), frame.getFunctionCode(), frame.getExceptionCode()); return; } - // 1.3 未认证连接,丢弃 + + // 2. 自定义功能码(认证等扩展) + if (StrUtil.isNotEmpty(frame.getCustomData())) { + handleCustomFrame(socket, frame, frameFormat); + return; + } + + // 1.2 未认证连接,丢弃 + // TODO @AI:把 1.2、1.3 拿到 handlePollingResponse 里;是否需要登录,自己知道! if (!connectionManager.isAuthenticated(socket)) { - // TODO @AI:这种需要返回一个结果给 modbus client? log.warn("[handleFrame][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress()); return; } - // TODO @AI:获取不到,看看要不要也打个告警;然后 - // 2. 标准 Modbus 响应 + // 3. DONE @AI:断言必须是云端轮询(不再支持主动上报) + // TODO @AI:貌似只能轮询到一次?! + // 1.3 标准 Modbus 响应(只支持云端轮询模式) + // TODO @AI:可以把 ConnectionInfo info = connectionManager.getConnectionInfo(socket); if (info == null) { + log.warn("[handleFrame][已认证但连接信息为空, remoteAddress={}]", socket.remoteAddress()); return; } - // TODO @AI:可以断言下,必须是云端轮询; - if (info.getMode() != null && info.getMode().equals(IotModbusModeEnum.ACTIVE_REPORT.getMode())) { - // mode=2:主动上报,透传 - handleActiveReport(info, frame); - } else { - // mode=1:云端轮询,匹配 PendingRequest - handlePollingResponse(info, frame, frameFormat); - } + handlePollingResponse(info, frame, frameFormat); } + // ========== 自定义 FC 处理(认证等) ========== + /** * 处理自定义功能码帧 + *

                              + * 异常分层翻译,参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAbstractHandler} */ private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { try { - // TODO @AI:直接使用 JsonUtils 去解析出 IotDeviceMessage JSONObject json = JSONUtil.parseObj(frame.getCustomData()); String method = json.getStr("method"); - // TODO @AI: method 枚举下; - if ("auth".equals(method)) { + if (METHOD_AUTH.equals(method)) { handleAuth(socket, frame, json, frameFormat); return; } - // TODO @AI:把 frame 都打印下; - log.warn("[handleCustomFrame][未知 method: {}]", method); + log.warn("[handleCustomFrame][未知 method: {}, frame: slaveId={}, FC={}, customData={}]", + method, frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData()); + } catch (ServiceException e) { + // 已知业务异常,返回对应的错误码和错误信息 + sendCustomResponse(socket, frame, frameFormat, e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + // 参数校验异常,返回 400 错误 + sendCustomResponse(socket, frame, frameFormat, BAD_REQUEST.getCode(), e.getMessage()); } catch (Exception e) { - // TODO @AI:各种情况的翻译;看看怎么弄比较合适;是不是要用 fc 自定义的 callback 下? - log.error("[handleCustomFrame][解析自定义 FC 数据失败]", e); + // 其他未知异常,返回 500 错误 + log.error("[handleCustomFrame][解析自定义 FC 数据失败, frame: slaveId={}, FC={}, customData={}]", + frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData(), e); + sendCustomResponse(socket, frame, frameFormat, + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); } } + // TODO @芋艿:在 review 下这个类; + // TODO @AI:不传递 json,直接在 frame /** * 处理认证请求 */ private void handleAuth(NetSocket socket, IotModbusFrame frame, JSONObject json, IotModbusFrameFormatEnum frameFormat) { - // TODO @AI:参数为空的校验; + // TODO @AI:是不是可以 JsonUtils.convert(json, IotDeviceAuthReqDTO.class); JSONObject params = json.getJSONObject("params"); if (params == null) { - sendAuthResponse(socket, frame, frameFormat, 1, "params 为空"); - return; + throw invalidParamException("params 不能为空"); } - // TODO @AI:参数判空; + // DONE @AI:参数判空 String clientId = params.getStr("clientId"); String username = params.getStr("username"); String password = params.getStr("password"); + // TODO @AI:逐个判空; + if (StrUtil.hasBlank(clientId, username, password)) { + throw invalidParamException("clientId、username、password 不能为空"); + } - try { - // 1. 调用认证 API - IotDeviceAuthReqDTO authReq = new IotDeviceAuthReqDTO() - .setClientId(clientId).setUsername(username).setPassword(password); - CommonResult authResult = deviceApi.authDevice(authReq); - // TODO @AI:应该不用 close 吧?! - // TODO @AI:BooleanUtils.isFalse - if (authResult == null || !authResult.isSuccess() || !Boolean.TRUE.equals(authResult.getData())) { - log.warn("[handleAuth][认证失败, clientId={}, username={}]", clientId, username); - sendAuthResponse(socket, frame, frameFormat, 1, "认证失败"); - socket.close(); - return; - } + // 1. 调用认证 API + IotDeviceAuthReqDTO authReq = new IotDeviceAuthReqDTO() + .setClientId(clientId).setUsername(username).setPassword(password); + CommonResult authResult = deviceApi.authDevice(authReq); + authResult.checkError(); + if (BooleanUtil.isFalse(authResult.getData())) { + log.warn("[handleAuth][认证失败, clientId={}, username={}]", clientId, username); + sendCustomResponse(socket, frame, frameFormat, 1, "认证失败"); + return; + } - // 2. 认证成功,查找设备配置(通过 username 作为 deviceName 查找) - // TODO 根据实际的认证模型优化查找逻辑 - // TODO @AI:通过 device - IotModbusDeviceConfigRespDTO config = configCacheService.findConfigByAuth(clientId, username, password); - if (config == null) { - // 退而求其次,遍历缓存查找 - log.info("[handleAuth][认证成功但未找到设备配置, clientId={}, username={}]", clientId, username); - } - // 2.2 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); - Assert.notNull(deviceInfo, "解析设备信息失败"); - // 2.3 获取设备信息 - // TODO @AI:这里要优化下,不要通过 spring 这样注入; - IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - Assert.notNull(device, "设备不存在"); - // TODO @AI:校验 frameFormat 是否一致;不一致,连接也失败; + // 2.1 认证成功,查找设备配置 + IotModbusDeviceConfigRespDTO config = configCacheService.findConfigByAuth(clientId, username, password); + if (config == null) { + log.info("[handleAuth][认证成功但未找到设备配置, clientId={}, username={}]", clientId, username); + } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + // DONE @AI:IotDeviceService 作为构造参数传入,不通过 SpringUtil.getBean + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); - // 3. 注册连接 - ConnectionInfo connectionInfo = new ConnectionInfo() - .setDeviceId(device.getId()) - .setSlaveId(frame.getSlaveId()) - .setFrameFormat(frameFormat) - .setMode(config != null ? config.getMode() : IotModbusModeEnum.POLLING.getMode()); + // 3. 注册连接 + ConnectionInfo connectionInfo = new ConnectionInfo() + .setDeviceId(device.getId()) + .setSlaveId(frame.getSlaveId()) + .setFrameFormat(frameFormat) + .setMode(config != null ? config.getMode() : IotModbusModeEnum.POLLING.getMode()); + if (config != null) { + connectionInfo.setDeviceId(config.getDeviceId()) + .setProductKey(config.getProductKey()) + .setDeviceName(config.getDeviceName()); + } + connectionManager.registerConnection(socket, connectionInfo); - if (config != null) { - connectionInfo.setDeviceId(config.getDeviceId()) - .setProductKey(config.getProductKey()) - .setDeviceName(config.getDeviceName()); - } - connectionManager.registerConnection(socket, connectionInfo); + // 4. 发送认证成功响应 + sendCustomResponse(socket, frame, frameFormat, 0, "success"); + log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", clientId, + config != null ? config.getDeviceId() : device.getId()); - // 4. 发送认证成功响应 - sendAuthResponse(socket, frame, frameFormat, 0, "success"); - log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", clientId, - config != null ? config.getDeviceId() : "unknown"); - - // 5. 回调:启动轮询等 - // TODO @AI:是不是不要 callback,而是主动调用! - if (onAuthSuccess != null && config != null) { - onAuthSuccess.accept(config.getDeviceId(), config); - } - } catch (Exception e) { - log.error("[handleAuth][认证异常]", e); - sendAuthResponse(socket, frame, frameFormat, 1, "认证异常"); - socket.close(); + // 5. 直接启动轮询 + if (config != null) { + pollScheduler.updatePolling(config); } } /** - * 发送认证响应 + * 发送自定义功能码响应 */ - private void sendAuthResponse(NetSocket socket, IotModbusFrame frame, - IotModbusFrameFormatEnum frameFormat, - int code, String message) { - // TODO @AI:不一定用 auth response;而是 custom? + private void sendCustomResponse(NetSocket socket, IotModbusFrame frame, + IotModbusFrameFormatEnum frameFormat, + int code, String message) { JSONObject resp = new JSONObject(); - resp.set("method", "auth"); + resp.set("method", METHOD_AUTH); resp.set("code", code); resp.set("message", message); - byte[] data = frameCodec.encodeCustomFrame(frame.getSlaveId(), resp.toString(), + byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), resp.toString(), frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0); connectionManager.sendToSocket(socket, data); } + // ========== 轮询响应处理 ========== + /** - * 处理轮询响应(mode=1) + * 处理轮询响应(云端轮询模式) */ private void handlePollingResponse(ConnectionInfo info, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { @@ -254,7 +262,7 @@ public class IotModbusTcpSlaveUpstreamHandler { return; } // 1.2 提取寄存器值 - int[] rawValues = IotModbusResponseParser.extractValues(frame); + int[] rawValues = IotModbusUtils.extractValues(frame); if (rawValues == null) { log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]", info.getDeviceId(), request.getIdentifier()); @@ -262,20 +270,17 @@ public class IotModbusTcpSlaveUpstreamHandler { } // 1.3 查找点位配置 IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId()); - if (config == null || config.getPoints() == null) { + if (config == null || CollUtil.isEmpty(config.getPoints())) { return; } - // TODO @AI:findone arrayUtil; - var point = config.getPoints().stream() - .filter(p -> p.getId().equals(request.getPointId())) - .findFirst().orElse(null); + var point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(request.getPointId())); if (point == null) { return; } - // TODO @AI:拆成 2.1、2.2 - // 4. 点位翻译 → 上报 + // 2.1 点位翻译 Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point); + // 2.2 上报属性 Map params = MapUtil.of(request.getIdentifier(), convertedValue); IotDeviceMessage message = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); @@ -284,29 +289,4 @@ public class IotModbusTcpSlaveUpstreamHandler { info.getDeviceId(), request.getIdentifier(), rawValues, convertedValue); } - // TODO @AI:不需要这个逻辑; - /** - * 处理主动上报(mode=2) - * 设备直接上报 property.report 格式:{propertyId: value},不做点位翻译 - */ - @SuppressWarnings("unchecked") - private void handleActiveReport(ConnectionInfo info, IotModbusFrame frame) { - // mode=2 下设备上报标准 Modbus 帧,但由于没有点位翻译, - // 这里暂时将原始寄存器值以 FC+地址 为 key 上报 - int[] rawValues = IotModbusResponseParser.extractValues(frame); - if (rawValues == null) { - return; - } - - // 简单上报:以 "register_FC{fc}" 作为属性名 - String propertyKey = "register_FC" + frame.getFunctionCode(); - Map params = MapUtil.of(propertyKey, rawValues); - IotDeviceMessage message = IotDeviceMessage.requestOf( - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); - messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId); - - log.debug("[handleActiveReport][设备={}, FC={}, 原始值={}]", - info.getDeviceId(), frame.getFunctionCode(), rawValues); - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java index 51246105c6..0c1ef64f48 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java @@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; import io.vertx.core.Vertx; @@ -37,7 +37,7 @@ public class IotModbusTcpSlavePollScheduler { private final Vertx vertx; private final IotModbusTcpSlaveConnectionManager connectionManager; - private final IotModbusFrameCodec frameCodec; + private final IotModbusFrameEncoder frameEncoder; private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; private final int requestTimeout; @@ -163,7 +163,7 @@ public class IotModbusTcpSlavePollScheduler { int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; // 2.2 编码读请求 - byte[] data = frameCodec.encodeReadRequest(slaveId, point.getFunctionCode(), + byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(), point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId); // 2.3 注册 PendingRequest PendingRequest pendingRequest = new PendingRequest( diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java index 0580909d05..f9cb923893 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java @@ -6,8 +6,9 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusRecordParserFactory; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusUtils; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetClient; @@ -59,7 +60,8 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { // ===================== 编解码器 ===================== - private static final IotModbusFrameCodec FRAME_CODEC = new IotModbusFrameCodec(CUSTOM_FC); + private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC); + private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC); // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== @@ -111,7 +113,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { // ===================== 轮询响应测试 ===================== /** - * 轮询响应测试:认证后等待网关下发 FC03 读请求(RTU 格式),构造读响应帧发回 + * 轮询响应测试:认证后持续监听网关下发的读请求(RTU 格式),每次收到都自动构造读响应帧发回 */ @Test public void testPollingResponse() throws Exception { @@ -121,30 +123,32 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { IotModbusFrame authResponse = authenticate(socket); log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); - // 2. 等待网关下发读请求 - log.info("[testPollingResponse][等待网关下发读请求...]"); - IotModbusFrame readRequest = waitForRequest(socket); - log.info("[testPollingResponse][收到读请求: slaveId={}, FC={}]", - readRequest.getSlaveId(), readRequest.getFunctionCode()); + // 2. 设置持续监听:每收到一个读请求,自动回复 + log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); + CompletableFuture done = new CompletableFuture<>(); + RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> { + log.info("[testPollingResponse][收到请求: slaveId={}, FC={}]", + frame.getSlaveId(), frame.getFunctionCode()); + // 解析读请求中的起始地址和数量 + byte[] pdu = frame.getPdu(); + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); - // 3. 解析读请求中的起始地址和数量 - byte[] pdu = readRequest.getPdu(); - int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); - int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); - log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); + // 构造读响应帧(模拟寄存器数据,RTU 格式) + int[] registerValues = new int[quantity]; + for (int i = 0; i < quantity; i++) { + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... + } + byte[] responseData = buildReadResponse(frame.getSlaveId(), + frame.getFunctionCode(), registerValues); + socket.write(Buffer.buffer(responseData)); + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); + }); + socket.handler(parser); - // 4. 构造读响应帧(模拟寄存器数据,RTU 格式) - int[] registerValues = new int[quantity]; - for (int i = 0; i < quantity; i++) { - registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... - } - byte[] responseData = buildReadResponse(readRequest.getSlaveId(), - readRequest.getFunctionCode(), registerValues); - socket.write(Buffer.buffer(responseData)); - log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); - - // 5. 等待一段时间让网关处理 - Thread.sleep(20000); + // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求 + Thread.sleep(200000); } finally { socket.close(); } @@ -199,23 +203,20 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { } /** - * 发送帧并等待响应(使用 IotModbusRecordParserFactory 自动检测帧格式) + * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码) */ private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { CompletableFuture responseFuture = new CompletableFuture<>(); - // 使用 RecordParserFactory 创建拆包器(自动检测帧格式) - RecordParser parser = IotModbusRecordParserFactory.create(CUSTOM_FC, - buffer -> { + // 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame) + RecordParser parser = FRAME_DECODER.createRecordParser( + (frame, frameFormat) -> { try { - // 检测到的帧格式应该是 RTU,使用 RTU 格式解码 - IotModbusFrame frame = FRAME_CODEC.decodeResponse( - buffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU); + log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat); responseFuture.complete(frame); } catch (Exception e) { responseFuture.completeExceptionally(e); } - }, - format -> log.info("[sendAndReceive][检测到帧格式: {}]", format)); + }); socket.handler(parser); // 发送请求 @@ -231,18 +232,16 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { */ private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { CompletableFuture requestFuture = new CompletableFuture<>(); - // 使用 RecordParserFactory 创建拆包器 - RecordParser parser = IotModbusRecordParserFactory.create(CUSTOM_FC, - buffer -> { + // 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame) + RecordParser parser = FRAME_DECODER.createRecordParser( + (frame, frameFormat) -> { try { - IotModbusFrame frame = FRAME_CODEC.decodeResponse( - buffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU); + log.info("[waitForRequest][检测到帧格式: {}]", frameFormat); requestFuture.complete(frame); } catch (Exception e) { requestFuture.completeExceptionally(e); } - }, - format -> log.info("[waitForRequest][检测到帧格式: {}]", format)); + }); socket.handler(parser); // 等待(超时 30 秒,因为轮询间隔可能比较长) @@ -264,7 +263,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { JSONObject json = new JSONObject(); json.set("method", "auth"); json.set("params", params); - return FRAME_CODEC.encodeCustomFrame(SLAVE_ID, json.toString(), + return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(), IotModbusFrameFormatEnum.MODBUS_RTU, 0); } @@ -286,7 +285,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF); } // 计算 CRC16 - int crc = IotModbusFrameCodec.calculateCrc16(frame, totalLength - 2); + int crc = IotModbusUtils.calculateCrc16(frame, totalLength - 2); frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High return frame; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java index 4d249927e7..c6c7ff28c3 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java @@ -6,8 +6,8 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; -import io.vertx.core.Handler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetClient; @@ -61,7 +61,8 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { // ===================== 编解码器 ===================== - private static final IotModbusFrameCodec FRAME_CODEC = new IotModbusFrameCodec(CUSTOM_FC); + private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC); + private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC); // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== @@ -113,7 +114,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { // ===================== 轮询响应测试 ===================== /** - * 轮询响应测试:认证后等待网关下发 FC03 读请求,构造读响应帧发回 + * 轮询响应测试:认证后持续监听网关下发的读请求,每次收到都自动构造读响应帧发回 */ @Test public void testPollingResponse() throws Exception { @@ -123,29 +124,31 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { IotModbusFrame authResponse = authenticate(socket); log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); - // 2. 等待网关下发读请求 - log.info("[testPollingResponse][等待网关下发读请求...]"); - IotModbusFrame readRequest = waitForRequest(socket); - log.info("[testPollingResponse][收到读请求: slaveId={}, FC={}, transactionId={}]", - readRequest.getSlaveId(), readRequest.getFunctionCode(), readRequest.getTransactionId()); + // 2. 设置持续监听:每收到一个读请求,自动回复 + log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); + CompletableFuture done = new CompletableFuture<>(); + RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> { + log.info("[testPollingResponse][收到请求: slaveId={}, FC={}, transactionId={}]", + frame.getSlaveId(), frame.getFunctionCode(), frame.getTransactionId()); + // 解析读请求中的起始地址和数量 + byte[] pdu = frame.getPdu(); + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); - // 3. 解析读请求中的起始地址和数量 - byte[] pdu = readRequest.getPdu(); - int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); - int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); - log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); + // 构造读响应帧(模拟寄存器数据) + int[] registerValues = new int[quantity]; + for (int i = 0; i < quantity; i++) { + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... + } + byte[] responseData = buildReadResponse(frame.getTransactionId(), + frame.getSlaveId(), frame.getFunctionCode(), registerValues); + socket.write(Buffer.buffer(responseData)); + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); + }); + socket.handler(parser); - // 4. 构造读响应帧(模拟寄存器数据) - int[] registerValues = new int[quantity]; - for (int i = 0; i < quantity; i++) { - registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... - } - byte[] responseData = buildReadResponse(readRequest.getTransactionId(), - readRequest.getSlaveId(), readRequest.getFunctionCode(), registerValues); - socket.write(Buffer.buffer(responseData)); - log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); - - // 5. 等待一段时间让网关处理 + // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求 Thread.sleep(200000); } finally { socket.close(); @@ -201,15 +204,20 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { } /** - * 发送帧并等待响应(MODBUS_TCP 格式) - *

                              - * 使用两阶段 RecordParser 拆包:fixedSizeMode(6) 读 MBAP 头 → fixedSizeMode(length) 读 body + * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码) */ private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { CompletableFuture responseFuture = new CompletableFuture<>(); - // 创建 TCP 两阶段拆包 RecordParser - RecordParser parser = RecordParser.newFixed(6); - parser.handler(new TcpRecordParserHandler(parser, responseFuture)); + // 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame) + RecordParser parser = FRAME_DECODER.createRecordParser( + (frame, frameFormat) -> { + try { + log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat); + responseFuture.complete(frame); + } catch (Exception e) { + responseFuture.completeExceptionally(e); + } + }); socket.handler(parser); // 发送请求 @@ -225,54 +233,22 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { */ private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { CompletableFuture requestFuture = new CompletableFuture<>(); - RecordParser parser = RecordParser.newFixed(6); - parser.handler(new TcpRecordParserHandler(parser, requestFuture)); + // 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame) + RecordParser parser = FRAME_DECODER.createRecordParser( + (frame, frameFormat) -> { + try { + log.info("[waitForRequest][检测到帧格式: {}]", frameFormat); + requestFuture.complete(frame); + } catch (Exception e) { + requestFuture.completeExceptionally(e); + } + }); socket.handler(parser); // 等待(超时 30 秒,因为轮询间隔可能比较长) return requestFuture.get(30000, TimeUnit.MILLISECONDS); } - /** - * MODBUS_TCP 两阶段拆包 Handler - */ - private class TcpRecordParserHandler implements Handler { - - private final RecordParser parser; - private final CompletableFuture future; - private byte[] mbapHeader; - private boolean waitingForBody = false; - - TcpRecordParserHandler(RecordParser parser, CompletableFuture future) { - this.parser = parser; - this.future = future; - } - - @Override - public void handle(Buffer buffer) { - try { - if (waitingForBody) { - // Phase 2: 收到 body(unitId + PDU) - byte[] body = buffer.getBytes(); - byte[] fullFrame = new byte[mbapHeader.length + body.length]; - System.arraycopy(mbapHeader, 0, fullFrame, 0, mbapHeader.length); - System.arraycopy(body, 0, fullFrame, mbapHeader.length, body.length); - - IotModbusFrame frame = FRAME_CODEC.decodeResponse(fullFrame, IotModbusFrameFormatEnum.MODBUS_TCP); - future.complete(frame); - } else { - // Phase 1: 收到 MBAP 头 6 字节 - this.mbapHeader = buffer.getBytes(); - int length = ((mbapHeader[4] & 0xFF) << 8) | (mbapHeader[5] & 0xFF); - this.waitingForBody = true; - parser.fixedSizeMode(length); - } - } catch (Exception e) { - future.completeExceptionally(e); - } - } - } - /** * 构造认证帧(MODBUS_TCP 格式) *

                              @@ -286,7 +262,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { JSONObject json = new JSONObject(); json.set("method", "auth"); json.set("params", params); - return FRAME_CODEC.encodeCustomFrame(SLAVE_ID, json.toString(), + return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(), IotModbusFrameFormatEnum.MODBUS_TCP, 1); } From 3ab33527e3d3ea0c7a11789d9585e3079e9ee46d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 8 Feb 2026 10:13:06 +0800 Subject: [PATCH 35/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9Amodbus-?= =?UTF-8?q?tcp-slave=20=E6=95=B4=E4=BD=93=E4=BB=A3=E7=A0=81=E8=BF=9B?= =?UTF-8?q?=E4=B8=80=E6=AD=A5=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../device/IotDeviceModbusPointDO.java | 3 +- .../core/biz/dto/IotModbusPointRespDTO.java | 3 +- .../core/enums/IotModbusFunctionCodeEnum.java | 60 ---- .../modbus/common/IotModbusUtils.java | 261 ++++++++++++++++++ .../tcpmaster/client/IotModbusTcpClient.java | 47 ++-- .../IotModbusTcpDownstreamHandler.java | 28 +- .../IotModbusTcpDownstreamSubscriber.java | 48 +--- .../tcpslave/IotModbusTcpSlaveConfig.java | 3 +- .../modbus/tcpslave/codec/IotModbusFrame.java | 27 +- .../tcpslave/codec/IotModbusFrameDecoder.java | 30 +- .../tcpslave/codec/IotModbusFrameEncoder.java | 1 + .../modbus/tcpslave/codec/IotModbusUtils.java | 127 --------- .../IotModbusTcpSlaveDownstreamHandler.java | 56 ++-- ...IotModbusTcpSlaveDownstreamSubscriber.java | 48 +--- .../IotModbusTcpSlaveUpstreamHandler.java | 160 +++++------ .../IotModbusTcpSlaveConfigCacheService.java | 2 +- ...odbusTcpSlaveModbusRtuIntegrationTest.java | 2 +- 17 files changed, 427 insertions(+), 479 deletions(-) delete mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFunctionCodeEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusUtils.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java index 60410848e2..dd3a3a9609 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; @@ -62,7 +61,7 @@ public class IotDeviceModbusPointDO extends TenantBaseDO { /** * Modbus 功能码 * - * 枚举 {@link IotModbusFunctionCodeEnum} + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器) */ private Integer functionCode; /** diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java index 129553dfd8..0424f1bc16 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.core.biz.dto; import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; import lombok.Data; @@ -33,7 +32,7 @@ public class IotModbusPointRespDTO { /** * Modbus 功能码 * - * 枚举 {@link IotModbusFunctionCodeEnum} + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器) */ private Integer functionCode; /** diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFunctionCodeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFunctionCodeEnum.java deleted file mode 100644 index 5487f51410..0000000000 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFunctionCodeEnum.java +++ /dev/null @@ -1,60 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.enums; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -import java.util.Arrays; - -/** - * IoT Modbus 功能码枚举 - * - * @author 芋道源码 - */ -@Getter -@RequiredArgsConstructor -public enum IotModbusFunctionCodeEnum implements ArrayValuable { - - READ_COILS(1, "读线圈", true, 5, 15), - READ_DISCRETE_INPUTS(2, "读离散输入", false, null, null), - READ_HOLDING_REGISTERS(3, "读保持寄存器", true, 6, 16), - READ_INPUT_REGISTERS(4, "读输入寄存器", false, null, null); - - public static final Integer[] ARRAYS = Arrays.stream(values()) - .map(IotModbusFunctionCodeEnum::getCode) - .toArray(Integer[]::new); - - /** - * 功能码 - */ - private final Integer code; - /** - * 名称 - */ - private final String name; - /** - * 是否支持写操作 - */ - private final Boolean writable; - /** - * 单个写功能码 - */ - private final Integer writeSingleCode; - /** - * 多个写功能码 - */ - private final Integer writeMultipleCode; - - @Override - public Integer[] array() { - return ARRAYS; - } - - public static IotModbusFunctionCodeEnum valueOf(Integer code) { - return Arrays.stream(values()) - .filter(e -> e.getCode().equals(code)) - .findFirst() - .orElse(null); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java new file mode 100644 index 0000000000..337c19fa93 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java @@ -0,0 +1,261 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT Modbus 工具类 + *

                              + * 提供: + * 1. Modbus 协议常量(功能码、掩码等) + * 2. CRC-16/MODBUS 计算和校验 + * 3. 功能码分类判断 + * 4. 从解码后的 ${IotModbusFrame} 中提取寄存器值(用于后续的点位翻译) + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusUtils { + + /** FC01: 读线圈 */ + public static final int FC_READ_COILS = 1; + /** FC02: 读离散输入 */ + public static final int FC_READ_DISCRETE_INPUTS = 2; + /** FC03: 读保持寄存器 */ + public static final int FC_READ_HOLDING_REGISTERS = 3; + /** FC04: 读输入寄存器 */ + public static final int FC_READ_INPUT_REGISTERS = 4; + + /** FC05: 写单个线圈 */ + public static final int FC_WRITE_SINGLE_COIL = 5; + /** FC06: 写单个寄存器 */ + public static final int FC_WRITE_SINGLE_REGISTER = 6; + /** FC15: 写多个线圈 */ + public static final int FC_WRITE_MULTIPLE_COILS = 15; + /** FC16: 写多个寄存器 */ + public static final int FC_WRITE_MULTIPLE_REGISTERS = 16; + + /** + * 异常响应掩码:响应帧的功能码最高位为 1 时,表示异常响应 + * 例如:请求 FC=0x03,异常响应 FC=0x83(0x03 | 0x80) + */ + public static final int FC_EXCEPTION_MASK = 0x80; + + /** + * 功能码掩码:用于从异常响应中提取原始功能码 + * 例如:异常 FC=0x83,原始 FC = 0x83 & 0x7F = 0x03 + */ + public static final int FC_MASK = 0x7F; + + // ==================== 功能码分类判断 ==================== + + /** + * 判断是否为读响应(FC01-04) + */ + public static boolean isReadResponse(int functionCode) { + return functionCode >= FC_READ_COILS && functionCode <= FC_READ_INPUT_REGISTERS; + } + + /** + * 判断是否为写响应(FC05/06/15/16) + */ + public static boolean isWriteResponse(int functionCode) { + return functionCode == FC_WRITE_SINGLE_COIL || functionCode == FC_WRITE_SINGLE_REGISTER + || functionCode == FC_WRITE_MULTIPLE_COILS || functionCode == FC_WRITE_MULTIPLE_REGISTERS; + } + + /** + * 判断是否为异常响应 + */ + public static boolean isExceptionResponse(int functionCode) { + return (functionCode & FC_EXCEPTION_MASK) != 0; + } + + /** + * 从异常响应中提取原始功能码 + */ + public static int extractOriginalFunctionCode(int exceptionFunctionCode) { + return exceptionFunctionCode & FC_MASK; + } + + /** + * 判断读功能码是否支持写操作 + *

                              + * FC01(读线圈)和 FC03(读保持寄存器)支持写操作; + * FC02(读离散输入)和 FC04(读输入寄存器)为只读。 + * + * @param readFunctionCode 读功能码(FC01-04) + * @return 是否支持写操作 + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean isWritable(int readFunctionCode) { + return readFunctionCode == FC_READ_COILS || readFunctionCode == FC_READ_HOLDING_REGISTERS; + } + + /** + * 获取单写功能码 + *

                              + * FC01(读线圈)→ FC05(写单个线圈); + * FC03(读保持寄存器)→ FC06(写单个寄存器); + * 其他返回 null(不支持写)。 + * + * @param readFunctionCode 读功能码 + * @return 单写功能码,不支持写时返回 null + */ + @SuppressWarnings("EnhancedSwitchMigration") + public static Integer getWriteSingleFunctionCode(int readFunctionCode) { + switch (readFunctionCode) { + case FC_READ_COILS: + return FC_WRITE_SINGLE_COIL; + case FC_READ_HOLDING_REGISTERS: + return FC_WRITE_SINGLE_REGISTER; + default: + return null; + } + } + + /** + * 获取多写功能码 + *

                              + * FC01(读线圈)→ FC15(写多个线圈); + * FC03(读保持寄存器)→ FC16(写多个寄存器); + * 其他返回 null(不支持写)。 + * + * @param readFunctionCode 读功能码 + * @return 多写功能码,不支持写时返回 null + */ + @SuppressWarnings("EnhancedSwitchMigration") + public static Integer getWriteMultipleFunctionCode(int readFunctionCode) { + switch (readFunctionCode) { + case FC_READ_COILS: + return FC_WRITE_MULTIPLE_COILS; + case FC_READ_HOLDING_REGISTERS: + return FC_WRITE_MULTIPLE_REGISTERS; + default: + return null; + } + } + + // ==================== 点位查找 ==================== + + /** + * 查找点位配置 + * + * @param config 设备 Modbus 配置 + * @param identifier 点位标识符 + * @return 匹配的点位配置,未找到返回 null + */ + public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { + return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); + } + + // ==================== CRC16 工具 ==================== + + /** + * 计算 CRC-16/MODBUS + * + * @param data 数据 + * @param length 计算长度 + * @return CRC16 值 + */ + public static int calculateCrc16(byte[] data, int length) { + int crc = 0xFFFF; + for (int i = 0; i < length; i++) { + crc ^= (data[i] & 0xFF); + for (int j = 0; j < 8; j++) { + if ((crc & 0x0001) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; + } + + /** + * 校验 CRC16 + * + * @param data 包含 CRC 的完整数据 + * @return 校验是否通过 + */ + public static boolean verifyCrc16(byte[] data) { + if (data.length < 3) { + return false; + } + int computed = calculateCrc16(data, data.length - 2); + int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8); + return computed == received; + } + + // ==================== 响应值提取 ==================== + + /** + * 从帧中提取寄存器值(FC01-04 读响应) + * + * @param frame 解码后的 Modbus 帧 + * @return 寄存器值数组(int[]),失败返回 null + */ + @SuppressWarnings("EnhancedSwitchMigration") + public static int[] extractValues(IotModbusFrame frame) { + if (frame == null || frame.isException()) { + return null; + } + byte[] pdu = frame.getPdu(); + if (pdu == null || pdu.length < 1) { + return null; + } + + int functionCode = frame.getFunctionCode(); + switch (functionCode) { + case FC_READ_COILS: + case FC_READ_DISCRETE_INPUTS: + return extractCoilValues(pdu); + case FC_READ_HOLDING_REGISTERS: + case FC_READ_INPUT_REGISTERS: + return extractRegisterValues(pdu); + default: + log.warn("[extractValues][不支持的功能码: {}]", functionCode); + return null; + } + } + + /** + * 提取线圈/离散输入值 + * PDU 格式(FC01/02 响应):[ByteCount(1)] [CoilStatus(N)] + */ + private static int[] extractCoilValues(byte[] pdu) { + if (pdu.length < 2) { + return null; + } + int byteCount = pdu[0] & 0xFF; + int bitCount = byteCount * 8; + int[] values = new int[bitCount]; + for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) { + values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01); + } + return values; + } + + /** + * 提取寄存器值 + * PDU 格式(FC03/04 响应):[ByteCount(1)] [RegisterData(N*2)] + */ + private static int[] extractRegisterValues(byte[] pdu) { + if (pdu.length < 2) { + return null; + } + int byteCount = pdu[0] & 0xFF; + int registerCount = byteCount / 2; + int[] values = new int[registerCount]; + for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) { + values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF); + } + return values; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java index f789c2c0cd..18fc211409 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; import com.ghgande.j2mod.modbus.msg.*; @@ -12,6 +11,8 @@ import com.ghgande.j2mod.modbus.util.BitVector; import io.vertx.core.Future; import lombok.extern.slf4j.Slf4j; +import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils.*; + // TODO @AI:感觉它更像一个工具类;但是名字叫 client 很奇怪; /** * IoT Modbus TCP 客户端 @@ -98,18 +99,14 @@ public class IotModbusTcpClient { */ @SuppressWarnings("EnhancedSwitchMigration") private ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); - if (functionCodeEnum == null) { - throw new IllegalArgumentException("不支持的功能码: " + functionCode); - } - switch (functionCodeEnum) { - case READ_COILS: + switch (functionCode) { + case FC_READ_COILS: return new ReadCoilsRequest(address, count); - case READ_DISCRETE_INPUTS: + case FC_READ_DISCRETE_INPUTS: return new ReadInputDiscretesRequest(address, count); - case READ_HOLDING_REGISTERS: + case FC_READ_HOLDING_REGISTERS: return new ReadMultipleRegistersRequest(address, count); - case READ_INPUT_REGISTERS: + case FC_READ_INPUT_REGISTERS: return new ReadInputRegistersRequest(address, count); default: throw new IllegalArgumentException("不支持的功能码: " + functionCode); @@ -119,13 +116,10 @@ public class IotModbusTcpClient { /** * 创建写入请求 */ + @SuppressWarnings("EnhancedSwitchMigration") private ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); - if (functionCodeEnum == null) { - throw new IllegalArgumentException("不支持的功能码: " + functionCode); - } - switch (functionCodeEnum) { - case READ_COILS: // 写线圈(使用功能码 5 或 15) + switch (functionCode) { + case FC_READ_COILS: // 写线圈(使用功能码 5 或 15) if (count == 1) { return new WriteCoilRequest(address, values[0] != 0); } else { @@ -135,7 +129,7 @@ public class IotModbusTcpClient { } return new WriteMultipleCoilsRequest(address, bv); } - case READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16) + case FC_READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16) if (count == 1) { return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0])); } else { @@ -145,8 +139,8 @@ public class IotModbusTcpClient { } return new WriteMultipleRegistersRequest(address, registers); } - case READ_DISCRETE_INPUTS: // 只读 - case READ_INPUT_REGISTERS: // 只读 + case FC_READ_DISCRETE_INPUTS: // 只读 + case FC_READ_INPUT_REGISTERS: // 只读 return null; default: throw new IllegalArgumentException("不支持的功能码: " + functionCode); @@ -156,13 +150,10 @@ public class IotModbusTcpClient { /** * 从响应中提取值 */ + @SuppressWarnings("EnhancedSwitchMigration") private int[] extractValues(ModbusResponse response, Integer functionCode) { - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); - if (functionCodeEnum == null) { - throw new IllegalArgumentException("不支持的功能码: " + functionCode); - } - switch (functionCodeEnum) { - case READ_COILS: + switch (functionCode) { + case FC_READ_COILS: ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; int bitCount = coilsResponse.getBitCount(); int[] coilValues = new int[bitCount]; @@ -170,7 +161,7 @@ public class IotModbusTcpClient { coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0; } return coilValues; - case READ_DISCRETE_INPUTS: + case FC_READ_DISCRETE_INPUTS: ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response; int discreteCount = discretesResponse.getBitCount(); int[] discreteValues = new int[discreteCount]; @@ -178,7 +169,7 @@ public class IotModbusTcpClient { discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0; } return discreteValues; - case READ_HOLDING_REGISTERS: + case FC_READ_HOLDING_REGISTERS: ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response; InputRegister[] holdingRegisters = holdingResponse.getRegisters(); int[] holdingValues = new int[holdingRegisters.length]; @@ -186,7 +177,7 @@ public class IotModbusTcpClient { holdingValues[i] = holdingRegisters[i].getValue(); } return holdingValues; - case READ_INPUT_REGISTERS: + case FC_READ_INPUT_REGISTERS: ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response; InputRegister[] inputRegisters = inputResponse.getRegisters(); int[] inputValues = new int[inputRegisters.length]; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java index c430fcbe95..b12fdfb3f7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java @@ -1,12 +1,11 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; -import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; @@ -60,19 +59,19 @@ public class IotModbusTcpDownstreamHandler { for (Map.Entry entry : propertyMap.entrySet()) { String identifier = entry.getKey(); Object value = entry.getValue(); - // 2.1.1 查找对应的点位配置 - IotModbusPointRespDTO point = findPoint(config, identifier); + // 2.1 查找对应的点位配置 + IotModbusPointRespDTO point = IotModbusUtils.findPoint(config, identifier); if (point == null) { log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); continue; } - // 2.1.2 检查是否支持写操作 - if (!isWritable(point.getFunctionCode())) { + // 2.2 检查是否支持写操作 + if (!IotModbusUtils.isWritable(point.getFunctionCode())) { log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); continue; } - // 2.2 执行写入 + // 2.3 执行写入 writeProperty(config, point, value); } } @@ -104,19 +103,4 @@ public class IotModbusTcpDownstreamHandler { config.getDeviceId(), point.getIdentifier(), e)); } - /** - * 查找点位配置 - */ - private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { - return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); - } - - /** - * 检查功能码是否支持写操作 - */ - private boolean isWritable(Integer functionCode) { - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); - return functionCodeEnum != null && Boolean.TRUE.equals(functionCodeEnum.getWritable()); - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java index d50608a0c8..c8e22aff79 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java @@ -1,61 +1,31 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -// TODO @AI:是不是可以继承 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java /** * IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j -public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber { +public class IotModbusTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { - private final IotModbusTcpMasterProtocol protocol; private final IotModbusTcpDownstreamHandler downstreamHandler; - private final IotMessageBus messageBus; - /** - * 启动订阅 - */ - public void start() { - messageBus.register(this); - log.info("[start][Modbus TCP Master 下行消息订阅器已启动, topic={}]", getTopic()); - } - - /** - * 停止订阅 - */ - public void stop() { - messageBus.unregister(this); - log.info("[stop][Modbus TCP Master 下行消息订阅器已停止]"); + public IotModbusTcpDownstreamSubscriber(IotModbusTcpMasterProtocol protocol, + IotModbusTcpDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; } @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - return getTopic(); // 点对点消费 - } - - @Override - public void onMessage(IotDeviceMessage message) { - log.debug("[onMessage][收到下行消息: {}]", message); - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败]", e); - } + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java index 25d377d2b8..60185f1eb0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; +import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -25,7 +26,7 @@ public class IotModbusTcpSlaveConfig { */ @NotNull(message = "自定义功能码不能为空") @Min(value = 65, message = "自定义功能码不能小于 65") - // TODO @AI:搞个范围; + @Max(value = 72, message = "自定义功能码不能大于 72") private Integer customFunctionCode = 65; /** diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java index a13c8148f2..6ac6930337 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; import lombok.Data; import lombok.experimental.Accessors; @@ -25,19 +27,19 @@ public class IotModbusFrame { */ private byte[] pdu; /** - * 事务标识符(TCP 模式特有) - * - * // TODO @AI:最好是 @某个类型独有; + * 事务标识符 + *

                              + * 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值, */ private Integer transactionId; /** - * 是否异常响应 - */ - private boolean exception; - // TODO @AI:是不是要枚举一些异常;另外,是不是覆盖掉 exception;因为只要判断有异常码是不是就可以了; - /** - * 异常码(当 exception=true 时有效) + * 异常码 + *

                              + * 当功能码最高位为 1 时(异常响应),此字段存储异常码。 + * 为 null 表示非异常响应。 + * + * @see IotModbusUtils#FC_EXCEPTION_MASK */ private Integer exceptionCode; @@ -46,4 +48,11 @@ public class IotModbusFrame { */ private String customData; + /** + * 是否异常响应(基于 exceptionCode 是否有值判断) + */ + public boolean isException() { + return exceptionCode != null; + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java index 8ea473738f..09d98e0dba 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; @@ -118,11 +119,8 @@ public class IotModbusFrameDecoder { .setPdu(pdu) .setTransactionId(transactionId); // 异常响应 - // TODO @AI:0x80 看看是不是要枚举; - if ((functionCode & 0x80) != 0) { - frame.setException(true); - // TODO @AI:0x7f 看看是不是要枚举; - frame.setFunctionCode(functionCode & 0x7F); + if (IotModbusUtils.isExceptionResponse(functionCode)) { + frame.setFunctionCode(IotModbusUtils.extractOriginalFunctionCode(functionCode)); if (pdu.length >= 1) { frame.setExceptionCode(pdu[0] & 0xFF); } @@ -247,7 +245,7 @@ public class IotModbusFrameDecoder { * 状态机流程: * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode * Phase 2: 根据 functionCode 确定剩余长度: - * - 异常响应 (FC & 0x80):fixedSizeMode(3) → exceptionCode(1) + CRC(2) + * - 异常响应 (FC & EXCEPTION_MASK):fixedSizeMode(3) → exceptionCode(1) + CRC(2) * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2) * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2) * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2) @@ -283,7 +281,7 @@ public class IotModbusFrameDecoder { this.slaveId = bytes[0]; this.functionCode = bytes[1]; int fc = functionCode & 0xFF; - if ((fc & 0x80) != 0) { + if (IotModbusUtils.isExceptionResponse(fc)) { // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节 // 已有 6 字节(多 1 字节),取前 5 字节组装 Buffer frame = Buffer.buffer(5); @@ -292,7 +290,7 @@ public class IotModbusFrameDecoder { frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC emitFrame(frame); resetToHeader(); - } else if (isReadResponse(fc) || fc == customFunctionCode) { + } else if (IotModbusUtils.isReadResponse(fc) || fc == customFunctionCode) { // 读响应或自定义 FC:bytes[2] = byteCount this.byteCount = bytes[2]; int bc = byteCount & 0xFF; @@ -317,7 +315,7 @@ public class IotModbusFrameDecoder { this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC parser.fixedSizeMode(remaining); } - } else if (isWriteResponse(fc)) { + } else if (IotModbusUtils.isWriteResponse(fc)) { // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节 // 已有 6 字节,还需 2 字节 state = STATE_WRITE_BODY; @@ -358,15 +356,15 @@ public class IotModbusFrameDecoder { this.slaveId = header[0]; this.functionCode = header[1]; int fc = functionCode & 0xFF; - if ((fc & 0x80) != 0) { + if (IotModbusUtils.isExceptionResponse(fc)) { // 异常响应 state = STATE_EXCEPTION_BODY; parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2) - } else if (isReadResponse(fc) || fc == customFunctionCode) { + } else if (IotModbusUtils.isReadResponse(fc) || fc == customFunctionCode) { // 读响应或自定义 FC state = STATE_READ_BYTE_COUNT; parser.fixedSizeMode(1); // byteCount - } else if (isWriteResponse(fc)) { + } else if (IotModbusUtils.isWriteResponse(fc)) { // 写响应 state = STATE_WRITE_BODY; pendingData = Buffer.buffer(); @@ -438,14 +436,6 @@ public class IotModbusFrameDecoder { parser.fixedSizeMode(2); // slaveId + FC } - // TODO @AI:可以抽到 IotModbusUtils 里? - private boolean isReadResponse(int fc) { - return fc >= 1 && fc <= 4; - } - - private boolean isWriteResponse(int fc) { - return fc == 5 || fc == 6 || fc == 15 || fc == 16; - } } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java index 0790ab3b36..723a57df8d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusUtils.java deleted file mode 100644 index f93f55cf9a..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusUtils.java +++ /dev/null @@ -1,127 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; - -import lombok.extern.slf4j.Slf4j; - -/** - * IoT Modbus 工具类 - *

                              - * 提供: - * 1. CRC-16/MODBUS 计算和校验 - * 2. 从解码后的 IotModbusFrame 中提取寄存器值(用于后续的点位翻译) - * - * @author 芋道源码 - */ -@Slf4j -public class IotModbusUtils { - - // TODO @AI:可以把 1、2、3、4、5 这些 fucntion code 在这里枚举下。 - // TODO @AI:一些枚举 0x80 这些可以这里枚举; - - // ==================== CRC16 工具 ==================== - - /** - * 计算 CRC-16/MODBUS - * - * @param data 数据 - * @param length 计算长度 - * @return CRC16 值 - */ - public static int calculateCrc16(byte[] data, int length) { - int crc = 0xFFFF; - for (int i = 0; i < length; i++) { - crc ^= (data[i] & 0xFF); - for (int j = 0; j < 8; j++) { - if ((crc & 0x0001) != 0) { - crc >>= 1; - crc ^= 0xA001; - } else { - crc >>= 1; - } - } - } - return crc; - } - - /** - * 校验 CRC16 - * - * @param data 包含 CRC 的完整数据 - * @return 校验是否通过 - */ - public static boolean verifyCrc16(byte[] data) { - if (data.length < 3) { - return false; - } - int computed = calculateCrc16(data, data.length - 2); - int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8); - return computed == received; - } - - // ==================== 响应值提取 ==================== - - /** - * 从帧中提取寄存器值(FC01-04 读响应) - * - * @param frame 解码后的 Modbus 帧 - * @return 寄存器值数组(int[]),失败返回 null - */ - @SuppressWarnings("EnhancedSwitchMigration") - public static int[] extractValues(IotModbusFrame frame) { - if (frame == null || frame.isException()) { - return null; - } - byte[] pdu = frame.getPdu(); - if (pdu == null || pdu.length < 1) { - return null; - } - - // TODO @AI:jmodbus 看看有没可以复用的枚举类 - int functionCode = frame.getFunctionCode(); - switch (functionCode) { - case 1: // Read Coils - case 2: // Read Discrete Inputs - return extractCoilValues(pdu); - case 3: // Read Holding Registers - case 4: // Read Input Registers - return extractRegisterValues(pdu); - default: - log.warn("[extractValues][不支持的功能码: {}]", functionCode); - return null; - } - } - - /** - * 提取线圈/离散输入值 - * PDU 格式(FC01/02 响应):[ByteCount(1)] [CoilStatus(N)] - */ - private static int[] extractCoilValues(byte[] pdu) { - if (pdu.length < 2) { - return null; - } - int byteCount = pdu[0] & 0xFF; - int bitCount = byteCount * 8; - int[] values = new int[bitCount]; - for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) { - values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01); - } - return values; - } - - /** - * 提取寄存器值 - * PDU 格式(FC03/04 响应):[ByteCount(1)] [RegisterData(N*2)] - */ - private static int[] extractRegisterValues(byte[] pdu) { - if (pdu.length < 2) { - return null; - } - int byteCount = pdu[0] & 0xFF; - int registerCount = byteCount / 2; - int[] values = new int[registerCount]; - for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) { - values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF); - } - return values; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java index 5a47a09c16..56c3478ed9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java @@ -1,13 +1,14 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream; -import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; @@ -18,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; -// TODO @AI:看看能不能和 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java 有一些复用逻辑; /** * IoT Modbus TCP Slave 下行消息处理器 *

                              @@ -48,7 +48,7 @@ public class IotModbusTcpSlaveDownstreamHandler { @SuppressWarnings("unchecked") public void handle(IotDeviceMessage message) { // 1.1 检查是否是属性设置消息 - if (!IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod().equals(message.getMethod())) { + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); return; } @@ -76,13 +76,13 @@ public class IotModbusTcpSlaveDownstreamHandler { String identifier = entry.getKey(); Object value = entry.getValue(); // 2.1 查找对应的点位配置 - IotModbusPointRespDTO point = findPoint(config, identifier); + IotModbusPointRespDTO point = IotModbusUtils.findPoint(config, identifier); if (point == null) { log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); continue; } // 2.2 检查是否支持写操作 - if (!isWritable(point.getFunctionCode())) { + if (!IotModbusUtils.isWritable(point.getFunctionCode())) { log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); continue; } @@ -97,56 +97,36 @@ public class IotModbusTcpSlaveDownstreamHandler { */ private void writeProperty(Long deviceId, ConnectionInfo connInfo, IotModbusPointRespDTO point, Object value) { - // 1. 转换属性值为原始值 + // 1.1 转换属性值为原始值 int[] rawValues = dataConverter.convertToRawValues(value, point); - // 2. 确定帧格式和事务 ID + // 1.2 确定帧格式和事务 ID IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); - if (frameFormat == null) { - frameFormat = IotModbusFrameFormatEnum.MODBUS_TCP; - } + Assert.notNull(frameFormat, "连接帧格式不能为空"); int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; - - // 3. 编码写请求 + // 1.3 编码写请求 byte[] data; - IotModbusFunctionCodeEnum fcEnum = IotModbusFunctionCodeEnum.valueOf(point.getFunctionCode()); - if (fcEnum == null) { - log.warn("[writeProperty][未知功能码: {}]", point.getFunctionCode()); - return; - } - if (rawValues.length == 1 && fcEnum.getWriteSingleCode() != null) { + int readFunctionCode = point.getFunctionCode(); + Integer writeSingleCode = IotModbusUtils.getWriteSingleFunctionCode(readFunctionCode); + Integer writeMultipleCode = IotModbusUtils.getWriteMultipleFunctionCode(readFunctionCode); + if (rawValues.length == 1 && writeSingleCode != null) { // 单个值:使用单写功能码(FC05/FC06) - data = frameEncoder.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(), + data = frameEncoder.encodeWriteSingleRequest(slaveId, writeSingleCode, point.getRegisterAddress(), rawValues[0], frameFormat, transactionId); - } else if (fcEnum.getWriteMultipleCode() != null) { + } else if (writeMultipleCode != null) { // 多个值:使用多写功能码(FC15/FC16) data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId, point.getRegisterAddress(), rawValues, frameFormat, transactionId); } else { - log.warn("[writeProperty][点位 {} 不支持写操作]", point.getIdentifier()); + log.warn("[writeProperty][点位 {} 不支持写操作, 功能码={}]", point.getIdentifier(), readFunctionCode); return; } - // 4. 发送 + // 2. 发送 connectionManager.sendToDevice(deviceId, data); log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", deviceId, point.getIdentifier(), value); } - /** - * 查找点位配置 - */ - private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { - return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); - } - - /** - * 检查功能码是否支持写操作 - */ - private boolean isWritable(Integer functionCode) { - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); - return functionCodeEnum != null && Boolean.TRUE.equals(functionCodeEnum.getWritable()); - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java index 5c74c27a9d..4e7b882770 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java @@ -1,61 +1,31 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -// TODO @AI:是不是可以继承 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java /** * IoT Modbus TCP Slave 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j -public class IotModbusTcpSlaveDownstreamSubscriber implements IotMessageSubscriber { +public class IotModbusTcpSlaveDownstreamSubscriber extends IotProtocolDownstreamSubscriber { - private final IotModbusTcpSlaveProtocol protocol; private final IotModbusTcpSlaveDownstreamHandler downstreamHandler; - private final IotMessageBus messageBus; - /** - * 启动订阅 - */ - public void start() { - messageBus.register(this); - log.info("[start][Modbus TCP Slave 下行消息订阅器已启动, topic={}]", getTopic()); - } - - /** - * 停止订阅 - */ - public void stop() { - messageBus.unregister(this); - log.info("[stop][Modbus TCP Slave 下行消息订阅器已停止]"); + public IotModbusTcpSlaveDownstreamSubscriber(IotModbusTcpSlaveProtocol protocol, + IotModbusTcpSlaveDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; } @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - return getTopic(); // 点对点消费 - } - - @Override - public void onMessage(IotDeviceMessage message) { - log.debug("[onMessage][收到下行消息: {}]", message); - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败]", e); - } + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java index 15ae18f996..69aebe98d3 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java @@ -5,14 +5,15 @@ import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; @@ -20,9 +21,9 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; @@ -40,14 +41,12 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; -// DONE @AI:逻辑有点多,看看是不是分区域! => 已按区域划分:认证 / 轮询响应 /** * IoT Modbus TCP Slave 上行数据处理器 *

                              * 处理: * 1. 自定义 FC 认证 - * 2. 轮询响应(mode=1)→ 点位翻译 → thing.property.post - * // DONE @AI:不用主动上报;主动上报走标准 tcp 即可 + * 2. 轮询响应 → 点位翻译 → thing.property.post * * @author 芋道源码 */ @@ -105,29 +104,14 @@ public class IotModbusTcpSlaveUpstreamHandler { return; } - // 2. 自定义功能码(认证等扩展) + // 2. 情况一:自定义功能码(认证等扩展) if (StrUtil.isNotEmpty(frame.getCustomData())) { handleCustomFrame(socket, frame, frameFormat); return; } - // 1.2 未认证连接,丢弃 - // TODO @AI:把 1.2、1.3 拿到 handlePollingResponse 里;是否需要登录,自己知道! - if (!connectionManager.isAuthenticated(socket)) { - log.warn("[handleFrame][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress()); - return; - } - - // 3. DONE @AI:断言必须是云端轮询(不再支持主动上报) - // TODO @AI:貌似只能轮询到一次?! - // 1.3 标准 Modbus 响应(只支持云端轮询模式) - // TODO @AI:可以把 - ConnectionInfo info = connectionManager.getConnectionInfo(socket); - if (info == null) { - log.warn("[handleFrame][已认证但连接信息为空, remoteAddress={}]", socket.remoteAddress()); - return; - } - handlePollingResponse(info, frame, frameFormat); + // 3. 情况二:标准 Modbus 响应 → 轮询响应处理 + handlePollingResponse(socket, frame, frameFormat); } // ========== 自定义 FC 处理(认证等) ========== @@ -138,96 +122,83 @@ public class IotModbusTcpSlaveUpstreamHandler { * 异常分层翻译,参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAbstractHandler} */ private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { + String method = null; try { - JSONObject json = JSONUtil.parseObj(frame.getCustomData()); - String method = json.getStr("method"); + IotDeviceMessage message = JsonUtils.parseObject(frame.getCustomData(), IotDeviceMessage.class); + if (message == null) { + throw invalidParamException("自定义 FC 数据解析失败"); + } + method = message.getMethod(); if (METHOD_AUTH.equals(method)) { - handleAuth(socket, frame, json, frameFormat); + handleAuth(socket, frame, frameFormat, message.getParams()); return; } log.warn("[handleCustomFrame][未知 method: {}, frame: slaveId={}, FC={}, customData={}]", method, frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData()); } catch (ServiceException e) { // 已知业务异常,返回对应的错误码和错误信息 - sendCustomResponse(socket, frame, frameFormat, e.getCode(), e.getMessage()); + sendCustomResponse(socket, frame, frameFormat, method, e.getCode(), e.getMessage()); } catch (IllegalArgumentException e) { // 参数校验异常,返回 400 错误 - sendCustomResponse(socket, frame, frameFormat, BAD_REQUEST.getCode(), e.getMessage()); + sendCustomResponse(socket, frame, frameFormat, method, BAD_REQUEST.getCode(), e.getMessage()); } catch (Exception e) { // 其他未知异常,返回 500 错误 log.error("[handleCustomFrame][解析自定义 FC 数据失败, frame: slaveId={}, FC={}, customData={}]", frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData(), e); - sendCustomResponse(socket, frame, frameFormat, + sendCustomResponse(socket, frame, frameFormat, method, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); } } - // TODO @芋艿:在 review 下这个类; - // TODO @AI:不传递 json,直接在 frame /** * 处理认证请求 */ - private void handleAuth(NetSocket socket, IotModbusFrame frame, JSONObject json, - IotModbusFrameFormatEnum frameFormat) { - // TODO @AI:是不是可以 JsonUtils.convert(json, IotDeviceAuthReqDTO.class); - JSONObject params = json.getJSONObject("params"); - if (params == null) { - throw invalidParamException("params 不能为空"); - } - // DONE @AI:参数判空 - String clientId = params.getStr("clientId"); - String username = params.getStr("username"); - String password = params.getStr("password"); - // TODO @AI:逐个判空; - if (StrUtil.hasBlank(clientId, username, password)) { - throw invalidParamException("clientId、username、password 不能为空"); - } + @SuppressWarnings("DataFlowIssue") + private void handleAuth(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat, Object params) { + // 1. 解析认证参数 + IotDeviceAuthReqDTO request = JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); + Assert.notNull(request, "认证参数不能为空"); + Assert.notBlank(request.getClientId(), "clientId 不能为空"); + Assert.notBlank(request.getUsername(), "username 不能为空"); + Assert.notBlank(request.getPassword(), "password 不能为空"); - // 1. 调用认证 API - IotDeviceAuthReqDTO authReq = new IotDeviceAuthReqDTO() - .setClientId(clientId).setUsername(username).setPassword(password); - CommonResult authResult = deviceApi.authDevice(authReq); - authResult.checkError(); - if (BooleanUtil.isFalse(authResult.getData())) { - log.warn("[handleAuth][认证失败, clientId={}, username={}]", clientId, username); - sendCustomResponse(socket, frame, frameFormat, 1, "认证失败"); + // 2.1 调用认证 API + CommonResult result = deviceApi.authDevice(request); + result.checkError(); + if (BooleanUtil.isFalse(result.getData())) { + log.warn("[handleAuth][认证失败, clientId={}, username={}]", request.getClientId(), request.getUsername()); + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, 1, "认证失败"); return; } - - // 2.1 认证成功,查找设备配置 - IotModbusDeviceConfigRespDTO config = configCacheService.findConfigByAuth(clientId, username, password); - if (config == null) { - log.info("[handleAuth][认证成功但未找到设备配置, clientId={}, username={}]", clientId, username); - } // 2.2 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(request.getUsername()); Assert.notNull(deviceInfo, "解析设备信息失败"); // 2.3 获取设备信息 - // DONE @AI:IotDeviceService 作为构造参数传入,不通过 SpringUtil.getBean IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); Assert.notNull(device, "设备不存在"); - // 3. 注册连接 + // 3.1 注册连接 ConnectionInfo connectionInfo = new ConnectionInfo() .setDeviceId(device.getId()) + .setProductKey(deviceInfo.getProductKey()) + .setDeviceName(deviceInfo.getDeviceName()) .setSlaveId(frame.getSlaveId()) - .setFrameFormat(frameFormat) - .setMode(config != null ? config.getMode() : IotModbusModeEnum.POLLING.getMode()); - if (config != null) { - connectionInfo.setDeviceId(config.getDeviceId()) - .setProductKey(config.getProductKey()) - .setDeviceName(config.getDeviceName()); - } + .setFrameFormat(frameFormat); connectionManager.registerConnection(socket, connectionInfo); + // 3.2 发送上线消息 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + messageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); + // 3.3 发送成功响应 + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, + GlobalErrorCodeConstants.SUCCESS.getCode(), "success"); + log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", request.getClientId(), device.getId()); - // 4. 发送认证成功响应 - sendCustomResponse(socket, frame, frameFormat, 0, "success"); - log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", clientId, - config != null ? config.getDeviceId() : device.getId()); - - // 5. 直接启动轮询 + // 4. 加载设备配置并启动轮询 + IotModbusDeviceConfigRespDTO config = configCacheService.loadDeviceConfig(device.getId()); if (config != null) { pollScheduler.updatePolling(config); + } else { + log.warn("[handleAuth][认证成功但未找到设备配置, deviceId={}]", device.getId()); } } @@ -236,12 +207,13 @@ public class IotModbusTcpSlaveUpstreamHandler { */ private void sendCustomResponse(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat, - int code, String message) { - JSONObject resp = new JSONObject(); - resp.set("method", METHOD_AUTH); - resp.set("code", code); - resp.set("message", message); - byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), resp.toString(), + String method, int code, String message) { + Map response = MapUtil.builder() + .put("method", method) + .put("code", code) + .put("message", message) + .build(); + byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), JsonUtils.toJsonString(response), frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0); connectionManager.sendToSocket(socket, data); } @@ -251,9 +223,16 @@ public class IotModbusTcpSlaveUpstreamHandler { /** * 处理轮询响应(云端轮询模式) */ - private void handlePollingResponse(ConnectionInfo info, IotModbusFrame frame, + private void handlePollingResponse(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { - // 1.1 匹配 PendingRequest + // 1. 获取连接信息(未认证连接丢弃) + ConnectionInfo info = connectionManager.getConnectionInfo(socket); + if (info == null) { + log.warn("[handlePollingResponse][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress()); + return; + } + + // 2.1 匹配 PendingRequest PendingRequest request = pendingRequestManager.matchResponse( info.getDeviceId(), frame, frameFormat); if (request == null) { @@ -261,26 +240,27 @@ public class IotModbusTcpSlaveUpstreamHandler { info.getDeviceId(), frame.getFunctionCode()); return; } - // 1.2 提取寄存器值 + // 2.2 提取寄存器值 int[] rawValues = IotModbusUtils.extractValues(frame); if (rawValues == null) { log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]", info.getDeviceId(), request.getIdentifier()); return; } - // 1.3 查找点位配置 + // 2.3 查找点位配置 IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId()); if (config == null || CollUtil.isEmpty(config.getPoints())) { return; } - var point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(request.getPointId())); + IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(), + p -> p.getId().equals(request.getPointId())); if (point == null) { return; } - // 2.1 点位翻译 + // 3.1 点位翻译 Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point); - // 2.2 上报属性 + // 3.2 上报属性 Map params = MapUtil.of(request.getIdentifier(), convertedValue); IotDeviceMessage message = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java index 382e8ab56e..16f1e47854 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java @@ -80,7 +80,7 @@ public class IotModbusTcpSlaveConfigCacheService { */ private List buildMockConfigs() { IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO(); - config.setDeviceId(1L); + config.setDeviceId(25L); config.setProductKey("4aymZgOTOOCrDKRT"); config.setDeviceName("small"); config.setSlaveId(1); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java index f9cb923893..faea89ee9b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetClient; From 4d578b239c8a1a7f3f2cc7c16f094787bac4169d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 8 Feb 2026 10:13:11 +0800 Subject: [PATCH 36/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9Amodbus-?= =?UTF-8?q?tcp-slave=20=E6=95=B4=E4=BD=93=E4=BB=A3=E7=A0=81=E8=BF=9B?= =?UTF-8?q?=E4=B8=80=E6=AD=A5=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tcpslave/IotModbusTcpSlaveProtocol.java | 43 ++-- .../IotModbusTcpSlaveUpstreamHandler.java | 1 - .../IotModbusTcpSlaveConfigCacheService.java | 187 ++++++++++-------- .../IotModbusTcpSlaveConnectionManager.java | 31 +-- ...otModbusTcpSlavePendingRequestManager.java | 28 ++- .../IotModbusTcpSlavePollScheduler.java | 17 +- 6 files changed, 157 insertions(+), 150 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java index ca317ec3fe..7b73b84ba2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java @@ -1,10 +1,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; @@ -32,9 +32,9 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; -// DONE @AI:不用主动上报! /** * IoT 网关 Modbus TCP Slave 协议 *

                              @@ -152,10 +152,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { } try { - // 1.1 首次加载配置 - // TODO @AI:可能首次不用加载;你在想想; - refreshConfig(); - // 1.2 启动配置刷新定时器 + // 1. 启动配置刷新定时器 int refreshInterval = slaveConfig.getConfigRefreshInterval(); configRefreshTimerId = vertx.setPeriodic( TimeUnit.SECONDS.toMillis(refreshInterval), @@ -286,6 +283,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { } pollScheduler.stopPolling(info.getDeviceId()); pendingRequestManager.removeDevice(info.getDeviceId()); + configCacheService.removeConfig(info.getDeviceId()); log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", info.getDeviceId(), socket.remoteAddress()); }); @@ -297,35 +295,30 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { } /** - * 刷新配置 + * 刷新已连接设备的配置(定时调用) + *

                              + * 与 tcpmaster 不同,slave 只刷新已连接设备的配置,不做全量 diff。 + * 设备的新增(认证时)和删除(断连时)分别在 {@link #handleConnection} 中处理。 */ private synchronized void refreshConfig() { try { - // 1. 从 biz 拉取最新配置 - List configs = configCacheService.refreshConfig(); - log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); + // 1. 只刷新已连接设备的配置 + Set connectedDeviceIds = connectionManager.getConnectedDeviceIds(); + if (CollUtil.isEmpty(connectedDeviceIds)) { + return; + } + List configs = + configCacheService.refreshConnectedDeviceConfigList(connectedDeviceIds); + log.debug("[refreshConfig][刷新了 {} 个已连接设备的配置]", configs.size()); - // 2. 更新已连接设备的轮询任务(仅 mode=1) + // 2. 更新已连接设备的轮询任务 for (IotModbusDeviceConfigRespDTO config : configs) { try { - if (config.getMode() != null - && config.getMode().equals(IotModbusModeEnum.POLLING.getMode())) { - // 只有已连接的设备才启动轮询 - ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(config.getDeviceId()); - if (connInfo != null) { - pollScheduler.updatePolling(config); - } - } + pollScheduler.updatePolling(config); } catch (Exception e) { log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); } } - - // 3. 清理已删除设备的资源 - configCacheService.cleanupRemovedDevices(configs, deviceId -> { - pollScheduler.stopPolling(deviceId); - pendingRequestManager.removeDevice(deviceId); - }); } catch (Exception e) { log.error("[refreshConfig][刷新配置失败]", e); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java index 69aebe98d3..805c65fd0f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java @@ -16,7 +16,6 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java index 16f1e47854..c8d68153a8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; @@ -10,15 +11,18 @@ import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; - -// TODO @AI:和 IotModbusTcpConfigCacheService 基本一致?! /** * IoT Modbus TCP Slave 配置缓存服务 *

                              - * 负责:从 biz 拉取 Modbus 设备配置,缓存配置数据,并检测配置变更 + * 与 tcpmaster 的 {@code IotModbusTcpConfigCacheService} 不同: + * - tcpmaster 启动时拉全量配置 → 主动建连,需要全量 diff 检测新增/删除设备 + * - tcpslave 设备主动连接 → 认证时按需加载配置,断连时清理,定时刷新已连接设备的配置 + *

                              + * 配置生命周期: + * 1. 认证时:{@link #loadDeviceConfig(Long)} 按 deviceId 从 API 加载配置到缓存 + * 2. 断连时:{@link #removeConfig(Long)} 从缓存中移除 + * 3. 定时刷新:{@link #refreshConnectedDeviceConfigList(Set)} 批量刷新已连接设备的配置 * * @author 芋道源码 */ @@ -33,43 +37,118 @@ public class IotModbusTcpSlaveConfigCacheService { */ private final Map configCache = new ConcurrentHashMap<>(); - // TODO @AI:它的 diff 算法,是不是不用和 IotModbusTcpConfigCacheService 完全一致;更多是1)首次连接时,查找;2)断开连接,移除;3)定时轮询更新; - /** - * 已知的设备 ID 集合 - */ - private final Set knownDeviceIds = ConcurrentHashMap.newKeySet(); + // ==================== 按需加载(认证时) ==================== /** - * 刷新配置 + * 加载单个设备的配置(认证成功后调用) + *

                              + * 从远程 API 获取全量配置,然后按 deviceId 匹配。 + * 如果远程获取失败,尝试从 Mock 数据中匹配。 * - * @return 最新的配置列表 + * @param deviceId 设备 ID + * @return 设备配置,未找到返回 null */ - public List refreshConfig() { + public IotModbusDeviceConfigRespDTO loadDeviceConfig(Long deviceId) { try { - // 1. 从远程获取配置 - // TODO @AI:需要过滤下,只查找连接的设备列表;并且只有主动轮询的,才会处理;方法名,应该是 List 结尾; + // 1. 从远程 API 获取全量配置 + // TODO @AI:等待修复,不着急; CommonResult> result = deviceApi.getEnabledModbusDeviceConfigs(); - if (result == null || !result.isSuccess() || result.getData() == null) { - log.warn("[refreshConfig][获取 Modbus 配置失败: {}]", result); - return new ArrayList<>(configCache.values()); + if (result != null && result.isSuccess() && result.getData() != null) { + for (IotModbusDeviceConfigRespDTO config : result.getData()) { + // 顺便更新缓存(其他已连接设备也受益) + configCache.put(config.getDeviceId(), config); + if (config.getDeviceId().equals(deviceId)) { + return config; + } + } } - List configs = new ArrayList<>(result.getData()); - - // 2. 追加 Mock 测试数据(一次性测试用途) - // TODO @芋艿:测试完成后移除 - configs.addAll(buildMockConfigs()); - - // 3. 更新缓存 - for (IotModbusDeviceConfigRespDTO config : configs) { - configCache.put(config.getDeviceId(), config); - } - return configs; } catch (Exception e) { - log.error("[refreshConfig][刷新配置失败]", e); - return new ArrayList<>(configCache.values()); + log.error("[loadDeviceConfig][从远程获取配置失败, deviceId={}]", deviceId, e); + } + + // 2. 远程未找到,尝试 Mock 数据 + // TODO @芋艿:测试完成后移除 + for (IotModbusDeviceConfigRespDTO mockConfig : buildMockConfigs()) { + configCache.put(mockConfig.getDeviceId(), mockConfig); + if (mockConfig.getDeviceId().equals(deviceId)) { + return mockConfig; + } + } + + return configCache.get(deviceId); + } + + // ==================== 定时刷新(已连接设备) ==================== + + /** + * 刷新已连接设备的配置缓存 + *

                              + * 定时调用,从远程 API 拉取最新配置,只更新已连接设备的缓存。 + * + * @param connectedDeviceIds 当前已连接的设备 ID 集合 + * @return 已连接设备的最新配置列表 + */ + public List refreshConnectedDeviceConfigList(Set connectedDeviceIds) { + if (CollUtil.isEmpty(connectedDeviceIds)) { + return Collections.emptyList(); + } + try { + // 1. 从远程获取全量配置 + // TODO @AI:传递 ids 批量查询;需要分批啦; + CommonResult> result = deviceApi.getEnabledModbusDeviceConfigs(); + List allConfigs; + if (result != null && result.isSuccess() && result.getData() != null) { + allConfigs = new ArrayList<>(result.getData()); + } else { + log.warn("[refreshConnectedDeviceConfigList][获取 Modbus 配置失败: {}]", result); + allConfigs = new ArrayList<>(); + } + + // 2. 追加 Mock 测试数据 + // TODO @芋艿:测试完成后移除 + allConfigs.addAll(buildMockConfigs()); + + // 3. 只保留已连接设备的配置,更新缓存 + List connectedConfigs = new ArrayList<>(); + for (IotModbusDeviceConfigRespDTO config : allConfigs) { + if (connectedDeviceIds.contains(config.getDeviceId())) { + configCache.put(config.getDeviceId(), config); + connectedConfigs.add(config); + } + } + return connectedConfigs; + } catch (Exception e) { + log.error("[refreshConnectedDeviceConfigList][刷新配置失败]", e); + // 降级:返回缓存中已连接设备的配置 + List fallback = new ArrayList<>(); + for (Long deviceId : connectedDeviceIds) { + IotModbusDeviceConfigRespDTO config = configCache.get(deviceId); + if (config != null) { + fallback.add(config); + } + } + return fallback; } } + // ==================== 缓存操作 ==================== + + /** + * 获取设备配置 + */ + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { + return configCache.get(deviceId); + } + + /** + * 移除设备配置缓存(设备断连时调用) + */ + public void removeConfig(Long deviceId) { + configCache.remove(deviceId); + } + + // ==================== Mock 数据 ==================== + /** * 构建 Mock 测试配置数据(一次性测试用途) * @@ -123,50 +202,4 @@ public class IotModbusTcpSlaveConfigCacheService { return Collections.singletonList(config); } - /** - * 获取设备配置 - */ - public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { - return configCache.get(deviceId); - } - - // TODO @AI:这个逻辑,是不是非必须? - /** - * 通过 clientId + username + password 查找设备配置(认证用) - * 暂通过遍历缓存实现,后续可优化为索引 - */ - public IotModbusDeviceConfigRespDTO findConfigByAuth(String clientId, String username, String password) { - // TODO @芋艿:测试完成后移除 mock 逻辑,改为正式查找 - // Mock:通过 clientId(格式 productKey.deviceName)匹配缓存中的设备 - if (clientId != null && clientId.contains(".")) { - String[] parts = clientId.split("\\.", 2); - String productKey = parts[0]; - String deviceName = parts[1]; - for (IotModbusDeviceConfigRespDTO config : configCache.values()) { - if (productKey.equals(config.getProductKey()) && deviceName.equals(config.getDeviceName())) { - return config; - } - } - } - return null; - } - - /** - * 清理已删除设备的资源 - */ - public void cleanupRemovedDevices(List currentConfigs, Consumer cleanupAction) { - Set currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId); - Set removedDeviceIds = new HashSet<>(knownDeviceIds); - removedDeviceIds.removeAll(currentDeviceIds); - - for (Long deviceId : removedDeviceIds) { - log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); - configCache.remove(deviceId); - cleanupAction.accept(deviceId); - } - - knownDeviceIds.clear(); - knownDeviceIds.addAll(currentDeviceIds); - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java index b342158a15..53363242c0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java @@ -4,9 +4,11 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; import lombok.Data; +import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** @@ -33,6 +35,7 @@ public class IotModbusTcpSlaveConnectionManager { * 连接信息 */ @Data + @Accessors(chain = true) public static class ConnectionInfo { /** @@ -57,12 +60,6 @@ public class IotModbusTcpSlaveConnectionManager { */ private IotModbusFrameFormatEnum frameFormat; - // TODO @AI:mode 是否非必须?! - /** - * 模式:1-云端轮询 2-主动上报 - */ - private Integer mode; - } /** @@ -75,17 +72,6 @@ public class IotModbusTcpSlaveConnectionManager { info.getDeviceId(), socket.remoteAddress()); } - // TODO @芋艿:待定是不是要保留?! - /** - * 设置连接的帧格式(首帧检测后调用) - */ - public void setFrameFormat(NetSocket socket, IotModbusFrameFormatEnum frameFormat) { - ConnectionInfo info = connectionMap.get(socket); - if (info != null) { - info.setFrameFormat(frameFormat); - } - } - /** * 获取连接信息 */ @@ -101,12 +87,11 @@ public class IotModbusTcpSlaveConnectionManager { return socket != null ? connectionMap.get(socket) : null; } - // TODO @AI:不用判断连接是否认证; /** - * 判断连接是否已认证 + * 获取所有已连接设备的 ID 集合 */ - public boolean isAuthenticated(NetSocket socket) { - return connectionMap.containsKey(socket); + public Set getConnectedDeviceIds() { + return deviceSocketMap.keySet(); } /** @@ -130,8 +115,7 @@ public class IotModbusTcpSlaveConnectionManager { log.warn("[sendToDevice][设备 {} 没有连接]", deviceId); return; } - // TODO @AI:直接复用 sendToSocket 方法?! - socket.write(Buffer.buffer(data)); + sendToSocket(socket, data); } /** @@ -141,7 +125,6 @@ public class IotModbusTcpSlaveConnectionManager { socket.write(Buffer.buffer(data)); } - // TODO @AI:貌似别的都没这个,是不是可以去掉哈?! /** * 关闭所有连接 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java index f82ddbfcae..3e5b99706c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; import lombok.AllArgsConstructor; @@ -7,6 +8,7 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import java.util.Deque; +import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; @@ -66,26 +68,24 @@ public class IotModbusTcpSlavePendingRequestManager { public PendingRequest matchResponse(Long deviceId, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { Deque queue = pendingRequests.get(deviceId); - // TODO @AI:CollUtil.isEmpty(queue) - if (queue == null || queue.isEmpty()) { + if (CollUtil.isEmpty(queue)) { return null; } + // TCP 模式:按 transactionId 精确匹配 if (frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP && frame.getTransactionId() != null) { - // TCP 模式:按 transactionId 精确匹配 return matchByTransactionId(queue, frame.getTransactionId()); - } else { - // RTU 模式:FIFO,匹配 slaveId + functionCode - return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode()); } + // RTU 模式:FIFO,匹配 slaveId + functionCode + return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode()); } /** * 按 transactionId 匹配 */ private PendingRequest matchByTransactionId(Deque queue, int transactionId) { - // TODO @AI:需要兼容 jdk8; - for (var it = queue.iterator(); it.hasNext(); ) { + Iterator it = queue.iterator(); + while (it.hasNext()) { PendingRequest req = it.next(); if (req.getTransactionId() != null && req.getTransactionId() == transactionId) { it.remove(); @@ -99,8 +99,8 @@ public class IotModbusTcpSlavePendingRequestManager { * 按 FIFO 匹配 */ private PendingRequest matchByFifo(Deque queue, int slaveId, int functionCode) { - // TODO @AI:需要兼容 jdk8; - for (var it = queue.iterator(); it.hasNext(); ) { + Iterator it = queue.iterator(); + while (it.hasNext()) { PendingRequest req = it.next(); if (req.getSlaveId() == slaveId && req.getFunctionCode() == functionCode) { it.remove(); @@ -120,13 +120,11 @@ public class IotModbusTcpSlavePendingRequestManager { int removed = 0; while (!queue.isEmpty()) { PendingRequest req = queue.peekFirst(); - // TODO @AI:if return 减少括号层级; - if (req != null && req.getExpireAt() < now) { - queue.pollFirst(); - removed++; - } else { + if (req == null || req.getExpireAt() >= now) { break; // 队列有序,后面的没过期 } + queue.pollFirst(); + removed++; } if (removed > 0) { log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java index 0c1ef64f48..70bda9f82e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java @@ -19,12 +19,11 @@ import java.util.concurrent.atomic.AtomicInteger; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; -// TODO @AI:和 IotModbusTcpPollScheduler 很像,是不是可以做一些复用? /** * IoT Modbus TCP Slave 轮询调度器 *

                              - * 管理点位的轮询定时器,为 mode=1(云端轮询)的设备调度读取任务。 - * 与 tcpmaster 不同,这里不直接通过 j2mod 读取,而是: + * 管理点位的轮询定时器,为云端轮询模式的设备调度读取任务。 + * 与 tcpmaster 的 {@code IotModbusTcpPollScheduler} 不同,这里不通过 j2mod 直接读取,而是: * 1. 编码 Modbus 读请求帧 * 2. 通过 ConnectionManager 发送到设备的 TCP 连接 * 3. 将请求注册到 PendingRequestManager,等待设备响应 @@ -154,13 +153,15 @@ public class IotModbusTcpSlavePollScheduler { } // 2.1 确定帧格式和事务 ID - // TODO @AI:不允许为空! IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); if (frameFormat == null) { - frameFormat = IotModbusFrameFormatEnum.MODBUS_TCP; + log.warn("[pollPoint][设备 {} 帧格式为空,跳过轮询]", deviceId); + return; } - // TODO @AI:transactionId 需要根据设备来么?然后递增也根据 IotModbusFrameFormatEnum.MODBUS_TCP 提前判断; - int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; + // TODO @AI:是不是得按照设备递增? + Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP + ? (transactionIdCounter.incrementAndGet() & 0xFFFF) + : null; int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; // 2.2 编码读请求 byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(), @@ -170,7 +171,7 @@ public class IotModbusTcpSlavePollScheduler { deviceId, point.getId(), point.getIdentifier(), slaveId, point.getFunctionCode(), point.getRegisterAddress(), point.getRegisterCount(), - frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP ? transactionId : null, + transactionId, System.currentTimeMillis() + requestTimeout); pendingRequestManager.addRequest(pendingRequest); From e13cd545cced2a8f7ae11e3f80aeca5c428fcc08 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 8 Feb 2026 15:54:18 +0800 Subject: [PATCH 37/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9Amodbus-?= =?UTF-8?q?tcp-slave=20=E7=BB=A7=E7=BB=AD=E4=BC=98=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=90=84=E7=A7=8D=E8=BF=9E=E6=8E=A5=E7=9A=84?= =?UTF-8?q?=E8=BE=B9=E7=95=8C=E3=80=81=E8=BD=AE=E8=AF=A2=E7=9A=84=E9=97=B4?= =?UTF-8?q?=E9=9A=94=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modbus/common/IotModbusDataConverter.java | 287 ---------------- .../modbus/common/IotModbusUtils.java | 310 ++++++++++++++++-- .../tcpmaster/IotModbusTcpMasterProtocol.java | 53 ++- ...ient.java => IotModbusTcpClientUtils.java} | 24 +- .../IotModbusTcpDownstreamHandler.java | 9 +- .../upstream/IotModbusTcpUpstreamHandler.java | 7 +- .../IotModbusTcpConfigCacheService.java | 13 +- .../IotModbusTcpConnectionManager.java | 25 +- .../manager/IotModbusTcpPollScheduler.java | 178 ++++++++-- .../tcpslave/IotModbusTcpSlaveConfig.java | 6 + .../tcpslave/IotModbusTcpSlaveProtocol.java | 24 +- .../tcpslave/codec/IotModbusFrameEncoder.java | 66 +++- .../IotModbusTcpSlaveDownstreamHandler.java | 25 +- .../IotModbusTcpSlaveUpstreamHandler.java | 9 +- .../IotModbusTcpSlaveConfigCacheService.java | 19 +- .../IotModbusTcpSlaveConnectionManager.java | 25 +- ...otModbusTcpSlavePendingRequestManager.java | 12 +- .../IotModbusTcpSlavePollScheduler.java | 183 +++++++++-- ...odbusTcpSlaveModbusTcpIntegrationTest.java | 2 + 19 files changed, 816 insertions(+), 461 deletions(-) delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusDataConverter.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/{IotModbusTcpClient.java => IotModbusTcpClientUtils.java} (90%) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusDataConverter.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusDataConverter.java deleted file mode 100644 index 219f20a336..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusDataConverter.java +++ /dev/null @@ -1,287 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common; - -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; -import lombok.extern.slf4j.Slf4j; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * IoT Modbus 数据转换器 - *

                              - * 负责 Modbus 原始寄存器值与物模型属性值的相互转换: - * 1. 将 Modbus 原始寄存器值转换为物模型属性值 - * 2. 将物模型属性值转换为 Modbus 原始寄存器值 - * - * @author 芋道源码 - */ -@Slf4j -public class IotModbusDataConverter { - - /** - * 将原始值转换为物模型属性值 - * - * @param rawValues 原始值数组(寄存器值或线圈值) - * @param point 点位配置 - * @return 转换后的属性值 - */ - public Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) { - if (ArrayUtil.isEmpty(rawValues)) { - return null; - } - String rawDataType = point.getRawDataType(); - String byteOrder = point.getByteOrder(); - BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); - - // 1. 根据原始数据类型解析原始数值 - Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder); - if (rawNumber == null) { - return null; - } - - // 2. 应用缩放因子:实际值 = 原始值 × scale - BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale); - - // 3. 根据数据类型返回合适的 Java 类型 - return formatValue(actualValue, rawDataType); - } - - /** - * 将物模型属性值转换为原始寄存器值 - * - * @param propertyValue 属性值 - * @param point 点位配置 - * @return 原始值数组 - */ - public int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) { - if (propertyValue == null) { - return new int[0]; - } - String rawDataType = point.getRawDataType(); - String byteOrder = point.getByteOrder(); - BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); - int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1); - - // 1. 转换为 BigDecimal - BigDecimal actualValue = new BigDecimal(propertyValue.toString()); - - // 2. 应用缩放因子:原始值 = 实际值 ÷ scale - BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP); - - // 3. 根据原始数据类型编码为寄存器值 - return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount); - } - - /** - * 解析原始值 - */ - @SuppressWarnings("EnhancedSwitchMigration") - private Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) { - IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); - if (dataTypeEnum == null) { - log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); - return rawValues[0]; - } - switch (dataTypeEnum) { - case BOOLEAN: - return rawValues[0] != 0 ? 1 : 0; - case INT16: - return (short) rawValues[0]; - case UINT16: - return rawValues[0] & 0xFFFF; - case INT32: - return parseInt32(rawValues, byteOrder); - case UINT32: - return parseUint32(rawValues, byteOrder); - case FLOAT: - return parseFloat(rawValues, byteOrder); - case DOUBLE: - return parseDouble(rawValues, byteOrder); - default: - log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); - return rawValues[0]; - } - } - - private int parseInt32(int[] rawValues, String byteOrder) { - if (rawValues.length < 2) { - return rawValues[0]; - } - byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); - return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt(); - } - - private long parseUint32(int[] rawValues, String byteOrder) { - if (rawValues.length < 2) { - return rawValues[0] & 0xFFFFFFFFL; - } - byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); - return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL; - } - - private float parseFloat(int[] rawValues, String byteOrder) { - if (rawValues.length < 2) { - return (float) rawValues[0]; - } - byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); - return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat(); - } - - private double parseDouble(int[] rawValues, String byteOrder) { - if (rawValues.length < 4) { - return rawValues[0]; - } - byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder); - return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble(); - } - - /** - * 将寄存器值转换为字节数组 - */ - private byte[] registersToBytes(int[] registers, int count) { - byte[] bytes = new byte[count * 2]; - for (int i = 0; i < Math.min(registers.length, count); i++) { - bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF); - bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF); - } - return bytes; - } - - /** - * 根据字节序重排字节 - */ - @SuppressWarnings("EnhancedSwitchMigration") - private byte[] reorderBytes(byte[] bytes, String byteOrder) { - IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder); - // null 或者大端序,不需要调整 - if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) { - return bytes; - } - - // 其他字节序调整 - byte[] result = new byte[bytes.length]; - switch (byteOrderEnum) { - case BA: // 小端序(16 位) - if (bytes.length >= 2) { - result[0] = bytes[1]; - result[1] = bytes[0]; - } - break; - case CDAB: // 大端字交换(32 位) - if (bytes.length >= 4) { - result[0] = bytes[2]; - result[1] = bytes[3]; - result[2] = bytes[0]; - result[3] = bytes[1]; - } - break; - case DCBA: // 小端序(32 位) - if (bytes.length >= 4) { - result[0] = bytes[3]; - result[1] = bytes[2]; - result[2] = bytes[1]; - result[3] = bytes[0]; - } - break; - case BADC: // 小端字交换(32 位) - if (bytes.length >= 4) { - result[0] = bytes[1]; - result[1] = bytes[0]; - result[2] = bytes[3]; - result[3] = bytes[2]; - } - break; - default: - return bytes; - } - return result; - } - - /** - * 编码为寄存器值 - */ - @SuppressWarnings("EnhancedSwitchMigration") - private int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) { - IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); - if (dataTypeEnum == null) { - return new int[]{rawValue.intValue()}; - } - switch (dataTypeEnum) { - case BOOLEAN: - return new int[]{rawValue.intValue() != 0 ? 1 : 0}; - case INT16: - case UINT16: - return new int[]{rawValue.intValue() & 0xFFFF}; - case INT32: - case UINT32: - return encodeInt32(rawValue.intValue(), byteOrder); - case FLOAT: - return encodeFloat(rawValue.floatValue(), byteOrder); - case DOUBLE: - return encodeDouble(rawValue.doubleValue(), byteOrder); - default: - return new int[]{rawValue.intValue()}; - } - } - - private int[] encodeInt32(int value, String byteOrder) { - byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array(); - bytes = reorderBytes(bytes, byteOrder); - return bytesToRegisters(bytes); - } - - private int[] encodeFloat(float value, String byteOrder) { - byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array(); - bytes = reorderBytes(bytes, byteOrder); - return bytesToRegisters(bytes); - } - - private int[] encodeDouble(double value, String byteOrder) { - byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array(); - bytes = reorderBytes(bytes, byteOrder); - return bytesToRegisters(bytes); - } - - private int[] bytesToRegisters(byte[] bytes) { - int[] registers = new int[bytes.length / 2]; - for (int i = 0; i < registers.length; i++) { - registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF); - } - return registers; - } - - /** - * 格式化返回值 - */ - @SuppressWarnings("EnhancedSwitchMigration") - private Object formatValue(BigDecimal value, String rawDataType) { - IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); - if (dataTypeEnum == null) { - return value; - } - switch (dataTypeEnum) { - case BOOLEAN: - return value.intValue() != 0; - case INT16: - case INT32: - return value.intValue(); - case UINT16: - case UINT32: - return value.longValue(); - case FLOAT: - return value.floatValue(); - case DOUBLE: - return value.doubleValue(); - default: - return value; - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java index 337c19fa93..227a886f8b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java @@ -1,22 +1,38 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; +import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + /** - * IoT Modbus 工具类 + * IoT Modbus 协议工具类 *

                              - * 提供: - * 1. Modbus 协议常量(功能码、掩码等) - * 2. CRC-16/MODBUS 计算和校验 - * 3. 功能码分类判断 - * 4. 从解码后的 ${IotModbusFrame} 中提取寄存器值(用于后续的点位翻译) + * 提供 Modbus 协议全链路能力: + *

                                + *
                              • 协议常量:功能码(FC01~FC16)、异常掩码等
                              • + *
                              • 功能码判断:读/写/异常分类、可写判断、写功能码映射
                              • + *
                              • CRC-16/MODBUS 计算和校验
                              • + *
                              • 数据转换:原始值 ↔ 物模型属性值({@link #convertToPropertyValue} / {@link #convertToRawValues})
                              • + *
                              • 帧值提取:从 Modbus 帧提取寄存器/线圈值({@link #extractValues})
                              • + *
                              • 点位查找({@link #findPoint})
                              • + *
                              * * @author 芋道源码 */ +@UtilityClass @Slf4j public class IotModbusUtils { @@ -139,19 +155,6 @@ public class IotModbusUtils { } } - // ==================== 点位查找 ==================== - - /** - * 查找点位配置 - * - * @param config 设备 Modbus 配置 - * @param identifier 点位标识符 - * @return 匹配的点位配置,未找到返回 null - */ - public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { - return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); - } - // ==================== CRC16 工具 ==================== /** @@ -192,7 +195,253 @@ public class IotModbusUtils { return computed == received; } - // ==================== 响应值提取 ==================== + // ==================== 数据转换 ==================== + + /** + * 将原始值转换为物模型属性值 + * + * @param rawValues 原始值数组(寄存器值或线圈值) + * @param point 点位配置 + * @return 转换后的属性值 + */ + public static Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) { + if (ArrayUtil.isEmpty(rawValues)) { + return null; + } + String rawDataType = point.getRawDataType(); + String byteOrder = point.getByteOrder(); + BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); + + // 1. 根据原始数据类型解析原始数值 + Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder); + if (rawNumber == null) { + return null; + } + + // 2. 应用缩放因子:实际值 = 原始值 × scale + BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale); + + // 3. 根据数据类型返回合适的 Java 类型 + return formatValue(actualValue, rawDataType); + } + + /** + * 将物模型属性值转换为原始寄存器值 + * + * @param propertyValue 属性值 + * @param point 点位配置 + * @return 原始值数组 + */ + public static int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) { + if (propertyValue == null) { + return new int[0]; + } + String rawDataType = point.getRawDataType(); + String byteOrder = point.getByteOrder(); + BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); + int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1); + + // 1. 转换为 BigDecimal + BigDecimal actualValue = new BigDecimal(propertyValue.toString()); + + // 2. 应用缩放因子:原始值 = 实际值 ÷ scale + BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP); + + // 3. 根据原始数据类型编码为寄存器值 + return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount); + } + + @SuppressWarnings("EnhancedSwitchMigration") + private static Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) { + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); + if (dataTypeEnum == null) { + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); + return rawValues[0]; + } + switch (dataTypeEnum) { + case BOOLEAN: + return rawValues[0] != 0 ? 1 : 0; + case INT16: + return (short) rawValues[0]; + case UINT16: + return rawValues[0] & 0xFFFF; + case INT32: + return parseInt32(rawValues, byteOrder); + case UINT32: + return parseUint32(rawValues, byteOrder); + case FLOAT: + return parseFloat(rawValues, byteOrder); + case DOUBLE: + return parseDouble(rawValues, byteOrder); + default: + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); + return rawValues[0]; + } + } + + private static int parseInt32(int[] rawValues, String byteOrder) { + if (rawValues.length < 2) { + return rawValues[0]; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt(); + } + + private static long parseUint32(int[] rawValues, String byteOrder) { + if (rawValues.length < 2) { + return rawValues[0] & 0xFFFFFFFFL; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL; + } + + private static float parseFloat(int[] rawValues, String byteOrder) { + if (rawValues.length < 2) { + return (float) rawValues[0]; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat(); + } + + private static double parseDouble(int[] rawValues, String byteOrder) { + if (rawValues.length < 4) { + return rawValues[0]; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble(); + } + + private static byte[] registersToBytes(int[] registers, int count) { + byte[] bytes = new byte[count * 2]; + for (int i = 0; i < Math.min(registers.length, count); i++) { + bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF); + bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF); + } + return bytes; + } + + @SuppressWarnings("EnhancedSwitchMigration") + private static byte[] reorderBytes(byte[] bytes, String byteOrder) { + IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder); + // null 或者大端序,不需要调整 + if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) { + return bytes; + } + + // 其他字节序调整 + byte[] result = new byte[bytes.length]; + switch (byteOrderEnum) { + case BA: // 小端序(16 位) + if (bytes.length >= 2) { + result[0] = bytes[1]; + result[1] = bytes[0]; + } + break; + case CDAB: // 大端字交换(32 位) + if (bytes.length >= 4) { + result[0] = bytes[2]; + result[1] = bytes[3]; + result[2] = bytes[0]; + result[3] = bytes[1]; + } + break; + case DCBA: // 小端序(32 位) + if (bytes.length >= 4) { + result[0] = bytes[3]; + result[1] = bytes[2]; + result[2] = bytes[1]; + result[3] = bytes[0]; + } + break; + case BADC: // 小端字交换(32 位) + if (bytes.length >= 4) { + result[0] = bytes[1]; + result[1] = bytes[0]; + result[2] = bytes[3]; + result[3] = bytes[2]; + } + break; + default: + return bytes; + } + return result; + } + + @SuppressWarnings("EnhancedSwitchMigration") + private static int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) { + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); + if (dataTypeEnum == null) { + return new int[]{rawValue.intValue()}; + } + switch (dataTypeEnum) { + case BOOLEAN: + return new int[]{rawValue.intValue() != 0 ? 1 : 0}; + case INT16: + case UINT16: + return new int[]{rawValue.intValue() & 0xFFFF}; + case INT32: + case UINT32: + return encodeInt32(rawValue.intValue(), byteOrder); + case FLOAT: + return encodeFloat(rawValue.floatValue(), byteOrder); + case DOUBLE: + return encodeDouble(rawValue.doubleValue(), byteOrder); + default: + return new int[]{rawValue.intValue()}; + } + } + + private static int[] encodeInt32(int value, String byteOrder) { + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array(); + bytes = reorderBytes(bytes, byteOrder); + return bytesToRegisters(bytes); + } + + private static int[] encodeFloat(float value, String byteOrder) { + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array(); + bytes = reorderBytes(bytes, byteOrder); + return bytesToRegisters(bytes); + } + + private static int[] encodeDouble(double value, String byteOrder) { + byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array(); + bytes = reorderBytes(bytes, byteOrder); + return bytesToRegisters(bytes); + } + + private static int[] bytesToRegisters(byte[] bytes) { + int[] registers = new int[bytes.length / 2]; + for (int i = 0; i < registers.length; i++) { + registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF); + } + return registers; + } + + @SuppressWarnings("EnhancedSwitchMigration") + private static Object formatValue(BigDecimal value, String rawDataType) { + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); + if (dataTypeEnum == null) { + return value; + } + switch (dataTypeEnum) { + case BOOLEAN: + return value.intValue() != 0; + case INT16: + case INT32: + return value.intValue(); + case UINT16: + case UINT32: + return value.longValue(); + case FLOAT: + return value.floatValue(); + case DOUBLE: + return value.doubleValue(); + default: + return value; + } + } + + // ==================== 帧值提取 ==================== /** * 从帧中提取寄存器值(FC01-04 读响应) @@ -224,10 +473,6 @@ public class IotModbusUtils { } } - /** - * 提取线圈/离散输入值 - * PDU 格式(FC01/02 响应):[ByteCount(1)] [CoilStatus(N)] - */ private static int[] extractCoilValues(byte[] pdu) { if (pdu.length < 2) { return null; @@ -241,10 +486,6 @@ public class IotModbusUtils { return values; } - /** - * 提取寄存器值 - * PDU 格式(FC03/04 响应):[ByteCount(1)] [RegisterData(N*2)] - */ private static int[] extractRegisterValues(byte[] pdu) { if (pdu.length < 2) { return null; @@ -258,4 +499,17 @@ public class IotModbusUtils { return values; } + // ==================== 点位查找 ==================== + + /** + * 查找点位配置 + * + * @param config 设备 Modbus 配置 + * @param identifier 点位标识符 + * @return 匹配的点位配置,未找到返回 null + */ + public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { + return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java index 0d5e86edc0..7b435d74fa 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java @@ -6,11 +6,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler; @@ -70,6 +69,7 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { private final IotModbusTcpConfigCacheService configCacheService; private final IotModbusTcpPollScheduler pollScheduler; + private final IotDeviceMessageService messageService; public IotModbusTcpMasterProtocol(ProtocolProperties properties) { IotModbusTcpMasterConfig modbusTcpMasterConfig = properties.getModbusTcpMaster(); @@ -87,15 +87,14 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { this.configCacheService = new IotModbusTcpConfigCacheService(deviceApi); // 初始化 Handler - IotModbusDataConverter dataConverter = new IotModbusDataConverter(); - IotModbusTcpClient modbusClient = new IotModbusTcpClient(); - IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); - IotModbusTcpUpstreamHandler upstreamHandler = new IotModbusTcpUpstreamHandler(messageService, dataConverter, serverId); + this.messageService = SpringUtil.getBean(IotDeviceMessageService.class); + IotDeviceMessageService messageService = this.messageService; + IotModbusTcpUpstreamHandler upstreamHandler = new IotModbusTcpUpstreamHandler(messageService, serverId); IotModbusTcpDownstreamHandler downstreamHandler = new IotModbusTcpDownstreamHandler(connectionManager, - modbusClient, dataConverter, configCacheService); + configCacheService); // 初始化轮询调度器 - this.pollScheduler = new IotModbusTcpPollScheduler(vertx, connectionManager, modbusClient, upstreamHandler); + this.pollScheduler = new IotModbusTcpPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService); // 初始化下行消息订阅者 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); @@ -186,24 +185,54 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { */ private synchronized void refreshConfig() { try { - // 1. 从 biz 拉取最新配置 + // 1. 从 biz 拉取最新配置(API 失败时返回 null) List configs = configCacheService.refreshConfig(); + if (configs == null) { + log.warn("[refreshConfig][API 失败,跳过本轮刷新]"); + return; + } log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); // 2. 更新连接和轮询任务 for (IotModbusDeviceConfigRespDTO config : configs) { try { - // 2.1 确保连接存在 + // 2.1 检测是否为首次连接 + boolean isNewConnection = connectionManager.getConnection(config.getDeviceId()) == null; + // 2.2 确保连接存在 connectionManager.ensureConnection(config); - // 2.2 更新轮询任务 + // 2.3 首次建连成功后发送上线消息 + // TODO @AI:在这里判断上线 ,会不会有点奇怪??? + if (isNewConnection && connectionManager.getConnection(config.getDeviceId()) != null) { + try { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + messageService.sendDeviceMessage(onlineMessage, + config.getProductKey(), config.getDeviceName(), serverId); + } catch (Exception ex) { + log.error("[refreshConfig][发送设备上线消息失败, deviceId={}]", config.getDeviceId(), ex); + } + } + // 2.4 更新轮询任务 pollScheduler.updatePolling(config); } catch (Exception e) { log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); } } - // 3. 清理已删除设备的资源 + // 3. 清理已删除设备的资源(仅 API 成功时才执行) configCacheService.cleanupRemovedDevices(configs, deviceId -> { + // 3.1 发送设备下线消息 + // TODO @AI:在这里判断上线 ,会不会有点奇怪??? + IotModbusDeviceConfigRespDTO removedConfig = configCacheService.getConfig(deviceId); + if (removedConfig != null) { + try { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + messageService.sendDeviceMessage(offlineMessage, + removedConfig.getProductKey(), removedConfig.getDeviceName(), serverId); + } catch (Exception ex) { + log.error("[refreshConfig][发送设备下线消息失败, deviceId={}]", deviceId, ex); + } + } + // 3.2 停止轮询和移除连接 pollScheduler.stopPolling(deviceId); connectionManager.removeDevice(deviceId); }); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClientUtils.java similarity index 90% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClientUtils.java index 18fc211409..44728396ac 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClientUtils.java @@ -9,22 +9,24 @@ import com.ghgande.j2mod.modbus.procimg.Register; import com.ghgande.j2mod.modbus.procimg.SimpleRegister; import com.ghgande.j2mod.modbus.util.BitVector; import io.vertx.core.Future; +import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils.*; -// TODO @AI:感觉它更像一个工具类;但是名字叫 client 很奇怪; /** - * IoT Modbus TCP 客户端 + * IoT Modbus TCP 客户端工具类 *

                              - * 封装 Modbus 协议读写操作: - * 1. 封装 Modbus 读/写操作 - * 2. 根据功能码,执行对应的 Modbus 请求 + * 封装基于 j2mod 的 Modbus TCP 读写操作: + * 1. 根据功能码创建对应的 Modbus 读/写请求 + * 2. 通过 {@link IotModbusTcpConnectionManager.ModbusConnection} 执行事务 + * 3. 从响应中提取原始值 * * @author 芋道源码 */ +@UtilityClass @Slf4j -public class IotModbusTcpClient { +public class IotModbusTcpClientUtils { /** * 读取 Modbus 数据 @@ -34,7 +36,7 @@ public class IotModbusTcpClient { * @param point 点位配置 * @return 原始值(int 数组) */ - public Future read(IotModbusTcpConnectionManager.ModbusConnection connection, + public static Future read(IotModbusTcpConnectionManager.ModbusConnection connection, Integer slaveId, IotModbusPointRespDTO point) { return connection.executeBlocking(tcpConnection -> { @@ -68,7 +70,7 @@ public class IotModbusTcpClient { * @param values 要写入的值 * @return 是否成功 */ - public Future write(IotModbusTcpConnectionManager.ModbusConnection connection, + public static Future write(IotModbusTcpConnectionManager.ModbusConnection connection, Integer slaveId, IotModbusPointRespDTO point, int[] values) { @@ -98,7 +100,7 @@ public class IotModbusTcpClient { * 创建读取请求 */ @SuppressWarnings("EnhancedSwitchMigration") - private ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { + private static ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { switch (functionCode) { case FC_READ_COILS: return new ReadCoilsRequest(address, count); @@ -117,7 +119,7 @@ public class IotModbusTcpClient { * 创建写入请求 */ @SuppressWarnings("EnhancedSwitchMigration") - private ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { + private static ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { switch (functionCode) { case FC_READ_COILS: // 写线圈(使用功能码 5 或 15) if (count == 1) { @@ -151,7 +153,7 @@ public class IotModbusTcpClient { * 从响应中提取值 */ @SuppressWarnings("EnhancedSwitchMigration") - private int[] extractValues(ModbusResponse response, Integer functionCode) { + private static int[] extractValues(ModbusResponse response, Integer functionCode) { switch (functionCode) { case FC_READ_COILS: ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java index b12fdfb3f7..27df39eefa 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java @@ -4,9 +4,8 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClientUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; import lombok.RequiredArgsConstructor; @@ -28,8 +27,6 @@ import java.util.Map; public class IotModbusTcpDownstreamHandler { private final IotModbusTcpConnectionManager connectionManager; - private final IotModbusTcpClient modbusClient; - private final IotModbusDataConverter dataConverter; private final IotModbusTcpConfigCacheService configCacheService; /** @@ -94,9 +91,9 @@ public class IotModbusTcpDownstreamHandler { } // 2.1 转换属性值为原始值 - int[] rawValues = dataConverter.convertToRawValues(value, point); + int[] rawValues = IotModbusUtils.convertToRawValues(value, point); // 2.2 执行 Modbus 写入 - modbusClient.write(connection, slaveId, point, rawValues) + IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues) .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", config.getDeviceId(), point.getIdentifier(), value)) .onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]", diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java index d45de2644e..8762481ebc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java @@ -5,7 +5,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import lombok.extern.slf4j.Slf4j; @@ -20,14 +20,11 @@ import java.util.Map; public class IotModbusTcpUpstreamHandler { private final IotDeviceMessageService messageService; - private final IotModbusDataConverter dataConverter; private final String serverId; public IotModbusTcpUpstreamHandler(IotDeviceMessageService messageService, - IotModbusDataConverter dataConverter, String serverId) { this.messageService = messageService; - this.dataConverter = dataConverter; this.serverId = serverId; } @@ -43,7 +40,7 @@ public class IotModbusTcpUpstreamHandler { int[] rawValue) { try { // 1.1 转换原始值为物模型属性值 - Object convertedValue = dataConverter.convertToPropertyValue(rawValue, point); + Object convertedValue = IotModbusUtils.convertToPropertyValue(rawValue, point); log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]", config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue); // 1.2 构造属性上报消息 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConfigCacheService.java index 74130898f8..5a3386e1d2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConfigCacheService.java @@ -38,16 +38,13 @@ public class IotModbusTcpConfigCacheService { /** * 刷新配置 * - * @return 最新的配置列表 + * @return 最新的配置列表;API 失败时返回 null(调用方应跳过 cleanup) */ public List refreshConfig() { try { // 1. 从远程获取配置 CommonResult> result = deviceApi.getEnabledModbusDeviceConfigs(); - if (result == null || !result.isSuccess() || result.getData() == null) { - log.warn("[refreshConfig][获取 Modbus 配置失败: {}]", result); - return new ArrayList<>(configCache.values()); - } + result.checkError(); List configs = result.getData(); // 2. 更新缓存(注意:不在这里更新 knownDeviceIds,由 cleanupRemovedDevices 统一管理) @@ -57,7 +54,7 @@ public class IotModbusTcpConfigCacheService { return configs; } catch (Exception e) { log.error("[refreshConfig][刷新配置失败]", e); - return new ArrayList<>(configCache.values()); + return null; } } @@ -84,11 +81,11 @@ public class IotModbusTcpConfigCacheService { Set removedDeviceIds = new HashSet<>(knownDeviceIds); removedDeviceIds.removeAll(currentDeviceIds); - // 2. 清理已删除设备 + // 2. 清理已删除设备(先执行 cleanupAction,再从缓存移除,保证 action 中仍可获取 config) for (Long deviceId : removedDeviceIds) { log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); - configCache.remove(deviceId); cleanupAction.accept(deviceId); + configCache.remove(deviceId); } // 3. 更新已知设备 ID 集合为当前有效的设备 ID diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConnectionManager.java index 613c0b0438..3b4e104402 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConnectionManager.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import com.ghgande.j2mod.modbus.net.TCPMasterConnection; import io.vertx.core.Context; @@ -50,8 +51,15 @@ public class IotModbusTcpConnectionManager { * @param config 设备配置 */ public void ensureConnection(IotModbusDeviceConfigRespDTO config) { - // 1. 记录设备与连接的关系 + // 1.1 检查设备是否切换了 IP/端口,若是则先清理旧连接 String connectionKey = buildConnectionKey(config.getIp(), config.getPort()); + String oldConnectionKey = deviceConnectionMap.get(config.getDeviceId()); + if (oldConnectionKey != null && ObjUtil.notEqual(oldConnectionKey, connectionKey)) { + log.info("[ensureConnection][设备 {} IP/端口变更: {} -> {}, 清理旧连接]", + config.getDeviceId(), oldConnectionKey, connectionKey); + removeDevice(config.getDeviceId()); + } + // 1.2 记录设备与连接的关系 deviceConnectionMap.put(config.getDeviceId(), connectionKey); // 2. 情况一:连接已存在,添加设备引用 @@ -68,8 +76,15 @@ public class IotModbusTcpConnectionManager { log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey); return; } - // 3.2 创建新连接 + // 3.2 double-check:拿到锁后再次检查,避免并发创建重复连接 try { + connection = connectionPool.get(connectionKey); + if (connection != null) { + connection.addDevice(config.getDeviceId(), config.getSlaveId()); + lock.unlock(); + return; + } + // 3.3 创建新连接 connection = createConnection(config, lock); connectionPool.put(connectionKey, connection); log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey); @@ -154,10 +169,10 @@ public class IotModbusTcpConnectionManager { if (connection.getTcpConnection() != null) { connection.getTcpConnection().close(); } - // 安全释放锁:先检查锁存在且被锁定,再检查是否当前线程持有 + // 强制解锁,避免死锁(正常情况下应该不会发生锁未释放的情况) RLock lock = connection.getLock(); - if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) { - lock.unlock(); + if (lock != null && lock.isLocked()) { + lock.forceUnlock(); } log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey); } catch (Exception e) { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java index 2350ea5115..afe4a9caed 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java @@ -3,39 +3,70 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClientUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler; import io.vertx.core.Vertx; import lombok.AllArgsConstructor; import lombok.Data; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; // TODO @AI:类的命名上,要体现上 master。其它类似 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster 也要! /** - * IoT Modbus TCP 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 + * IoT Modbus TCP Master 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public class IotModbusTcpPollScheduler { private final Vertx vertx; private final IotModbusTcpConnectionManager connectionManager; - private final IotModbusTcpClient modbusClient; private final IotModbusTcpUpstreamHandler upstreamHandler; + private final IotModbusTcpConfigCacheService configCacheService; + + /** + * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积 + */ + private static final long MIN_REQUEST_INTERVAL = 100; /** * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) */ private final Map> devicePointTimers = new ConcurrentHashMap<>(); + /** + * per-device 请求队列:deviceId -> 待执行请求队列 + */ + private final Map> deviceRequestQueues = new ConcurrentHashMap<>(); + + /** + * per-device 上次请求时间戳:deviceId -> lastRequestTimeMs + */ + private final Map deviceLastRequestTime = new ConcurrentHashMap<>(); + + /** + * per-device 延迟 timer 标记:deviceId -> 是否有延迟 timer 在等待 + */ + private final Map deviceDelayTimerActive = new ConcurrentHashMap<>(); + + public IotModbusTcpPollScheduler(Vertx vertx, + IotModbusTcpConnectionManager connectionManager, + IotModbusTcpUpstreamHandler upstreamHandler, + IotModbusTcpConfigCacheService configCacheService) { + this.vertx = vertx; + this.connectionManager = connectionManager; + this.upstreamHandler = upstreamHandler; + this.configCacheService = configCacheService; + } + + // ========== 点位定时器 ========== + /** * 点位定时器信息 */ @@ -54,20 +85,22 @@ public class IotModbusTcpPollScheduler { } + // ========== 轮询管理 ========== + /** * 更新轮询任务(增量更新) * * 1. 【删除】点位:停止对应的轮询定时器 * 2. 【新增】点位:创建对应的轮询定时器 * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器 - * 4. 其他属性变化(包括未变化的):不处理(下次轮询时自动使用新配置) + * 4. 其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point) */ public void updatePolling(IotModbusDeviceConfigRespDTO config) { Long deviceId = config.getDeviceId(); List newPoints = config.getPoints(); Map currentTimers = devicePointTimers .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>()); - // 1.1 计算新配置(包括新增和修改的点位)中的点位 ID 集合 + // 1.1 计算新配置中的点位 ID 集合 Set newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId); // 1.2 计算删除的点位 ID 集合 Set removedPointIds = new HashSet<>(currentTimers.keySet()); @@ -92,7 +125,7 @@ public class IotModbusTcpPollScheduler { PointTimerInfo existingTimer = currentTimers.get(pointId); // 3.1 新增点位:创建定时器 if (existingTimer == null) { - Long timerId = createPollTimer(config, point); + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); if (timerId != null) { currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", @@ -101,7 +134,7 @@ public class IotModbusTcpPollScheduler { } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { // 3.2 pollInterval 变化:重建定时器 vertx.cancelTimer(existingTimer.getTimerId()); - Long timerId = createPollTimer(config, point); + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); if (timerId != null) { currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", @@ -110,51 +143,136 @@ public class IotModbusTcpPollScheduler { currentTimers.remove(pointId); } } - // 3.3 其他属性变化:不处理(下次轮询时自动使用新配置) + // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置 } } /** * 创建轮询定时器 + *

                              + * 闭包只捕获 deviceId 和 pointId,运行时从 configCache 获取最新配置,避免旧快照问题。 */ - private Long createPollTimer(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { - if (point.getPollInterval() == null || point.getPollInterval() <= 0) { + private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) { + if (pollInterval == null || pollInterval <= 0) { return null; } - return vertx.setPeriodic(point.getPollInterval(), timerId -> { + return vertx.setPeriodic(pollInterval, timerId -> { try { - pollPoint(config, point); + submitPollRequest(deviceId, pointId); } catch (Exception e) { - log.error("[createPollTimer][轮询点位失败, deviceId={}, identifier={}]", - config.getDeviceId(), point.getIdentifier(), e); + log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e); } }); } + // ========== 请求队列(per-device 限速) ========== + + /** + * 提交轮询请求到设备请求队列(保证同设备请求间隔) + */ + private void submitPollRequest(Long deviceId, Long pointId) { + Queue queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>()); + queue.offer(() -> pollPoint(deviceId, pointId)); + processDeviceQueue(deviceId); + } + + /** + * 处理设备请求队列 + */ + private void processDeviceQueue(Long deviceId) { + Queue queue = deviceRequestQueues.get(deviceId); + if (CollUtil.isEmpty(queue)) { + return; + } + // 检查是否已有延迟 timer 在等待 + if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) { + return; + } + long now = System.currentTimeMillis(); + long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L); + long elapsed = now - lastTime; + if (elapsed >= MIN_REQUEST_INTERVAL) { + // 满足间隔要求,立即执行 + Runnable task = queue.poll(); + if (task != null) { + deviceLastRequestTime.put(deviceId, now); + task.run(); + // 继续处理队列中的下一个(如果有的话,需要延迟) + if (!queue.isEmpty()) { + scheduleNextRequest(deviceId); + } + } + } else { + // 需要延迟 + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed); + } + } + + private void scheduleNextRequest(Long deviceId) { + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL); + } + + private void scheduleNextRequest(Long deviceId, long delayMs) { + deviceDelayTimerActive.put(deviceId, true); + vertx.setTimer(delayMs, id -> { + deviceDelayTimerActive.put(deviceId, false); + Queue queue = deviceRequestQueues.get(deviceId); + if (CollUtil.isEmpty(queue)) { + Runnable task = queue.poll(); + if (task != null) { + deviceLastRequestTime.put(deviceId, System.currentTimeMillis()); + task.run(); + } + // 继续处理 + if (queue != null && !queue.isEmpty()) { + scheduleNextRequest(deviceId); + } + } + }); + } + + // ========== 轮询执行 ========== + /** * 轮询单个点位 */ - private void pollPoint(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { - // 1.1 获取连接 - IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); - if (connection == null) { - log.warn("[pollPoint][设备 {} 没有连接]", config.getDeviceId()); + private void pollPoint(Long deviceId, Long pointId) { + // 1.1 从 configCache 获取最新配置 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); + if (config == null || CollUtil.isEmpty(config.getPoints())) { + log.warn("[pollPoint][设备 {} 没有配置]", deviceId); return; } - // 1.2 获取 slave ID - Integer slaveId = connectionManager.getSlaveId(config.getDeviceId()); - if (slaveId == null) { - log.warn("[pollPoint][设备 {} 没有 slaveId]", config.getDeviceId()); + // 1.2 查找点位 + // TODO @AI:是不是这里,可以抽到 IotModbusUtils 里?感觉应该有几个地方需要的; + IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId)); + if (point == null) { + log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId); return; } - // 2. 执行 Modbus 读取 - modbusClient.read(connection, slaveId, point) + // 2.1 获取连接 + IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId); + if (connection == null) { + log.warn("[pollPoint][设备 {} 没有连接]", deviceId); + return; + } + // 2.2 获取 slave ID + Integer slaveId = connectionManager.getSlaveId(deviceId); + if (slaveId == null) { + log.warn("[pollPoint][设备 {} 没有 slaveId]", deviceId); + return; + } + + // 3. 执行 Modbus 读取 + IotModbusTcpClientUtils.read(connection, slaveId, point) .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue)) .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]", - config.getDeviceId(), point.getIdentifier(), e)); + deviceId, point.getIdentifier(), e)); } + // ========== 停止 ========== + /** * 停止设备的轮询 */ @@ -166,6 +284,10 @@ public class IotModbusTcpPollScheduler { for (PointTimerInfo timerInfo : timers.values()) { vertx.cancelTimer(timerInfo.getTimerId()); } + // 清理请求队列 + deviceRequestQueues.remove(deviceId); + deviceLastRequestTime.remove(deviceId); + deviceDelayTimerActive.remove(deviceId); log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java index 60185f1eb0..d21c28e676 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java @@ -41,4 +41,10 @@ public class IotModbusTcpSlaveConfig { @NotNull(message = "请求清理间隔不能为空") private Integer requestCleanupInterval = 10000; + // TODO @AI:可以去掉这个开关,因为本身就是模拟的,稍后我自己也会手动或者让你去掉(听我指令!) + /** + * 是否启用 Mock 测试数据(仅开发/测试环境使用,线上务必关闭) + */ + private Boolean mockEnabled = false; + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java index 7b73b84ba2..b042363acc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java @@ -7,10 +7,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler; @@ -34,6 +34,7 @@ import lombok.extern.slf4j.Slf4j; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; /** * IoT 网关 Modbus TCP Slave 协议 @@ -92,6 +93,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { private final IotModbusTcpSlaveUpstreamHandler upstreamHandler; private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; private final IotModbusTcpSlavePollScheduler pollScheduler; + private final IotDeviceMessageService messageService; public IotModbusTcpSlaveProtocol(ProtocolProperties properties) { this.slaveConfig = properties.getModbusTcpSlave(); @@ -112,24 +114,27 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode()); this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode()); + // 初始化共享事务 ID 自增器(PollScheduler 和 DownstreamHandler 共用,避免 transactionId 冲突) + AtomicInteger transactionIdCounter = new AtomicInteger(0); + // 初始化轮询调度器 this.pollScheduler = new IotModbusTcpSlavePollScheduler( vertx, connectionManager, frameEncoder, pendingRequestManager, - slaveConfig.getRequestTimeout()); + slaveConfig.getRequestTimeout(), transactionIdCounter, configCacheService); // 初始化 Handler - IotModbusDataConverter dataConverter = new IotModbusDataConverter(); - IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.messageService = SpringUtil.getBean(IotDeviceMessageService.class); + IotDeviceMessageService messageService = this.messageService; IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler( - deviceApi, messageService, dataConverter, frameEncoder, + deviceApi, messageService, frameEncoder, connectionManager, configCacheService, pendingRequestManager, pollScheduler, deviceService, serverId); // 初始化下行消息订阅者 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); IotModbusTcpSlaveDownstreamHandler downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( - connectionManager, configCacheService, dataConverter, frameEncoder); + connectionManager, configCacheService, frameEncoder, transactionIdCounter); this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber( this, downstreamHandler, messageBus); } @@ -284,6 +289,13 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { pollScheduler.stopPolling(info.getDeviceId()); pendingRequestManager.removeDevice(info.getDeviceId()); configCacheService.removeConfig(info.getDeviceId()); + // 发送设备下线消息 + try { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + messageService.sendDeviceMessage(offlineMessage, info.getProductKey(), info.getDeviceName(), serverId); + } catch (Exception ex) { + log.error("[handleConnection][发送设备下线消息失败, deviceId={}]", info.getDeviceId(), ex); + } log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", info.getDeviceId(), socket.remoteAddress()); }); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java index 723a57df8d..195c82352a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java @@ -8,9 +8,7 @@ import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; /** - * IoT Modbus 帧编码器 - *

                              - * 负责将 Modbus 请求/响应编码为字节数组,支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式。 + * IoT Modbus 帧编码器:负责将 Modbus 请求/响应编码为字节数组,支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式。 * * @author 芋道源码 */ @@ -30,11 +28,11 @@ public class IotModbusFrameEncoder { * @param startAddress 起始寄存器地址 * @param quantity 寄存器数量 * @param format 帧格式 - * @param transactionId 事务 ID(TCP 模式下使用) + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) * @return 编码后的字节数组 */ public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity, - IotModbusFrameFormatEnum format, int transactionId) { + IotModbusFrameFormatEnum format, Integer transactionId) { // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)] byte[] pdu = new byte[5]; pdu[0] = (byte) functionCode; @@ -48,16 +46,23 @@ public class IotModbusFrameEncoder { /** * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05) * + * DONE @AI:【from codex】【高】FC05 写线圈时,value 已转换为 Modbus 标准值(非0 → 0xFF00,0 → 0x0000); + * 新增 encodeWriteMultipleCoilsRequest 方法用于 FC15 编码(按 bit 打包)。 + * * @param slaveId 从站地址 * @param functionCode 功能码 * @param address 寄存器地址 * @param value 值 * @param format 帧格式 - * @param transactionId 事务 ID + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) * @return 编码后的字节数组 */ public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value, - IotModbusFrameFormatEnum format, int transactionId) { + IotModbusFrameFormatEnum format, Integer transactionId) { + // FC05 单写线圈:Modbus 标准要求 value 为 0xFF00(ON)或 0x0000(OFF) + if (functionCode == IotModbusUtils.FC_WRITE_SINGLE_COIL) { + value = (value != 0) ? 0xFF00 : 0x0000; + } // PDU: [FC(1)] [Address(2)] [Value(2)] byte[] pdu = new byte[5]; pdu[0] = (byte) functionCode; @@ -75,11 +80,11 @@ public class IotModbusFrameEncoder { * @param address 起始地址 * @param values 值数组 * @param format 帧格式 - * @param transactionId 事务 ID + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) * @return 编码后的字节数组 */ public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values, - IotModbusFrameFormatEnum format, int transactionId) { + IotModbusFrameFormatEnum format, Integer transactionId) { // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)] int quantity = values.length; int byteCount = quantity * 2; @@ -97,17 +102,50 @@ public class IotModbusFrameEncoder { return wrapFrame(slaveId, pdu, format, transactionId); } + /** + * 编码写多个线圈请求(FC15) + *

                              + * 按 Modbus FC15 标准,线圈值按 bit 打包(每个 byte 包含 8 个线圈状态)。 + * + * @param slaveId 从站地址 + * @param address 起始地址 + * @param values 线圈值数组(int[],非0 表示 ON,0 表示 OFF) + * @param format 帧格式 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) + * @return 编码后的字节数组 + */ + public byte[] encodeWriteMultipleCoilsRequest(int slaveId, int address, int[] values, + IotModbusFrameFormatEnum format, Integer transactionId) { + // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [CoilValues(N)] + int quantity = values.length; + int byteCount = (quantity + 7) / 8; // 向上取整 + byte[] pdu = new byte[6 + byteCount]; + pdu[0] = (byte) IotModbusUtils.FC_WRITE_MULTIPLE_COILS; // FC15 + pdu[1] = (byte) ((address >> 8) & 0xFF); + pdu[2] = (byte) (address & 0xFF); + pdu[3] = (byte) ((quantity >> 8) & 0xFF); + pdu[4] = (byte) (quantity & 0xFF); + pdu[5] = (byte) byteCount; + // 按 bit 打包:每个 byte 的 bit0 对应最低地址的线圈 + for (int i = 0; i < quantity; i++) { + if (values[i] != 0) { + pdu[6 + i / 8] |= (byte) (1 << (i % 8)); + } + } + return wrapFrame(slaveId, pdu, format, transactionId); + } + /** * 编码自定义功能码帧(认证响应等) * * @param slaveId 从站地址 * @param jsonData JSON 数据 * @param format 帧格式 - * @param transactionId 事务 ID + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) * @return 编码后的字节数组 */ public byte[] encodeCustomFrame(int slaveId, String jsonData, - IotModbusFrameFormatEnum format, int transactionId) { + IotModbusFrameFormatEnum format, Integer transactionId) { byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8); // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)] byte[] pdu = new byte[2 + jsonBytes.length]; @@ -125,12 +163,12 @@ public class IotModbusFrameEncoder { * @param slaveId 从站地址 * @param pdu PDU 数据(含 functionCode) * @param format 帧格式 - * @param transactionId 事务 ID(TCP 模式下使用) + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式可为 null) * @return 完整帧字节数组 */ - private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, int transactionId) { + private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, Integer transactionId) { if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { - return wrapTcpFrame(slaveId, pdu, transactionId); + return wrapTcpFrame(slaveId, pdu, transactionId != null ? transactionId : 0); } else { return wrapRtuFrame(slaveId, pdu); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java index 56c3478ed9..ca9edde53e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java @@ -7,13 +7,11 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import java.util.Map; @@ -28,19 +26,27 @@ import java.util.concurrent.atomic.AtomicInteger; * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public class IotModbusTcpSlaveDownstreamHandler { private final IotModbusTcpSlaveConnectionManager connectionManager; private final IotModbusTcpSlaveConfigCacheService configCacheService; - private final IotModbusDataConverter dataConverter; private final IotModbusFrameEncoder frameEncoder; /** - * TCP 事务 ID 自增器 + * TCP 事务 ID 自增器(与 PollScheduler 共享) */ - private final AtomicInteger transactionIdCounter = new AtomicInteger(0); + private final AtomicInteger transactionIdCounter; + + public IotModbusTcpSlaveDownstreamHandler(IotModbusTcpSlaveConnectionManager connectionManager, + IotModbusTcpSlaveConfigCacheService configCacheService, + IotModbusFrameEncoder frameEncoder, + AtomicInteger transactionIdCounter) { + this.connectionManager = connectionManager; + this.configCacheService = configCacheService; + this.frameEncoder = frameEncoder; + this.transactionIdCounter = transactionIdCounter; + } /** * 处理下行消息 @@ -98,12 +104,15 @@ public class IotModbusTcpSlaveDownstreamHandler { private void writeProperty(Long deviceId, ConnectionInfo connInfo, IotModbusPointRespDTO point, Object value) { // 1.1 转换属性值为原始值 - int[] rawValues = dataConverter.convertToRawValues(value, point); + int[] rawValues = IotModbusUtils.convertToRawValues(value, point); // 1.2 确定帧格式和事务 ID IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); Assert.notNull(frameFormat, "连接帧格式不能为空"); - int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; + // TODO @AI:【from 芋艿】需要和按照 deviceId 进行自增么??? + Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP + ? (transactionIdCounter.incrementAndGet() & 0xFFFF) + : null; int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; // 1.3 编码写请求 byte[] data; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java index 805c65fd0f..67b2d52e8f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java @@ -19,7 +19,6 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; @@ -56,7 +55,6 @@ public class IotModbusTcpSlaveUpstreamHandler { private final IotDeviceCommonApi deviceApi; private final IotDeviceMessageService messageService; - private final IotModbusDataConverter dataConverter; private final IotModbusFrameEncoder frameEncoder; private final IotModbusTcpSlaveConnectionManager connectionManager; private final IotModbusTcpSlaveConfigCacheService configCacheService; @@ -67,7 +65,6 @@ public class IotModbusTcpSlaveUpstreamHandler { public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi, IotDeviceMessageService messageService, - IotModbusDataConverter dataConverter, IotModbusFrameEncoder frameEncoder, IotModbusTcpSlaveConnectionManager connectionManager, IotModbusTcpSlaveConfigCacheService configCacheService, @@ -77,7 +74,6 @@ public class IotModbusTcpSlaveUpstreamHandler { String serverId) { this.deviceApi = deviceApi; this.messageService = messageService; - this.dataConverter = dataConverter; this.frameEncoder = frameEncoder; this.connectionManager = connectionManager; this.configCacheService = configCacheService; @@ -175,6 +171,7 @@ public class IotModbusTcpSlaveUpstreamHandler { // 2.3 获取设备信息 IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); Assert.notNull(device, "设备不存在"); + // TODO @AI:2.4 必须找到连接配置; // 3.1 注册连接 ConnectionInfo connectionInfo = new ConnectionInfo() @@ -213,7 +210,7 @@ public class IotModbusTcpSlaveUpstreamHandler { .put("message", message) .build(); byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), JsonUtils.toJsonString(response), - frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0); + frameFormat, frame.getTransactionId()); connectionManager.sendToSocket(socket, data); } @@ -258,7 +255,7 @@ public class IotModbusTcpSlaveUpstreamHandler { } // 3.1 点位翻译 - Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point); + Object convertedValue = IotModbusUtils.convertToPropertyValue(rawValues, point); // 3.2 上报属性 Map params = MapUtil.of(request.getIdentifier(), convertedValue); IotDeviceMessage message = IotDeviceMessage.requestOf( diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java index c8d68153a8..9a00997f05 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java @@ -66,12 +66,15 @@ public class IotModbusTcpSlaveConfigCacheService { log.error("[loadDeviceConfig][从远程获取配置失败, deviceId={}]", deviceId, e); } - // 2. 远程未找到,尝试 Mock 数据 + // 2. 远程未找到,尝试 Mock 数据(仅 mockEnabled=true 时) + // DONE @AI:【from codex】【中】Mock 数据已通过 mockEnabled 配置开关控制,线上环境不会污染真实配置。 // TODO @芋艿:测试完成后移除 - for (IotModbusDeviceConfigRespDTO mockConfig : buildMockConfigs()) { - configCache.put(mockConfig.getDeviceId(), mockConfig); - if (mockConfig.getDeviceId().equals(deviceId)) { - return mockConfig; + if (true) { + for (IotModbusDeviceConfigRespDTO mockConfig : buildMockConfigs()) { + configCache.put(mockConfig.getDeviceId(), mockConfig); + if (mockConfig.getDeviceId().equals(deviceId)) { + return mockConfig; + } } } @@ -104,9 +107,11 @@ public class IotModbusTcpSlaveConfigCacheService { allConfigs = new ArrayList<>(); } - // 2. 追加 Mock 测试数据 + // 2. 追加 Mock 测试数据(仅 mockEnabled=true 时) // TODO @芋艿:测试完成后移除 - allConfigs.addAll(buildMockConfigs()); + if (true) { + allConfigs.addAll(buildMockConfigs()); + } // 3. 只保留已连接设备的配置,更新缓存 List connectedConfigs = new ArrayList<>(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java index 53363242c0..241d8d777e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java @@ -66,6 +66,21 @@ public class IotModbusTcpSlaveConnectionManager { * 注册已认证的连接 */ public void registerConnection(NetSocket socket, ConnectionInfo info) { + // 先检查该设备是否有旧连接,若有且不是同一个 socket,关闭旧 socket + NetSocket oldSocket = deviceSocketMap.get(info.getDeviceId()); + if (oldSocket != null && oldSocket != socket) { + log.info("[registerConnection][设备 {} 存在旧连接, 关闭旧 socket, oldRemote={}, newRemote={}]", + info.getDeviceId(), oldSocket.remoteAddress(), socket.remoteAddress()); + connectionMap.remove(oldSocket); + try { + oldSocket.close(); + } catch (Exception e) { + // TODO @AI:这里日志可以打的更完整一点,方便追溯;比如:设备 ID、旧连接地址等 + log.warn("[registerConnection][关闭旧 socket 失败]", e); + } + } + + // 注册新连接 connectionMap.put(socket, info); deviceSocketMap.put(info.getDeviceId(), socket); log.info("[registerConnection][设备 {} 连接已注册, remoteAddress={}]", @@ -100,8 +115,14 @@ public class IotModbusTcpSlaveConnectionManager { public ConnectionInfo removeConnection(NetSocket socket) { ConnectionInfo info = connectionMap.remove(socket); if (info != null && info.getDeviceId() != null) { - deviceSocketMap.remove(info.getDeviceId()); - log.info("[removeConnection][设备 {} 连接已移除]", info.getDeviceId()); + // 使用两参数 remove:只有当 deviceSocketMap 中对应的 socket 就是当前 socket 时才删除, + // 避免新 socket 已注册后旧 socket 关闭时误删新映射 + boolean removed = deviceSocketMap.remove(info.getDeviceId(), socket); + if (removed) { + log.info("[removeConnection][设备 {} 连接已移除]", info.getDeviceId()); + } else { + log.info("[removeConnection][设备 {} 旧连接关闭, 新连接仍在线, 跳过清理]", info.getDeviceId()); + } } return info; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java index 3e5b99706c..8ddcd25598 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java @@ -118,13 +118,13 @@ public class IotModbusTcpSlavePendingRequestManager { for (Map.Entry> entry : pendingRequests.entrySet()) { Deque queue = entry.getValue(); int removed = 0; - while (!queue.isEmpty()) { - PendingRequest req = queue.peekFirst(); - if (req == null || req.getExpireAt() >= now) { - break; // 队列有序,后面的没过期 + Iterator it = queue.iterator(); + while (it.hasNext()) { + PendingRequest req = it.next(); + if (req.getExpireAt() < now) { + it.remove(); + removed++; } - queue.pollFirst(); - removed++; } if (removed > 0) { log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java index 70bda9f82e..d0f38e3653 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java @@ -10,11 +10,11 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotM import io.vertx.core.Vertx; import lombok.AllArgsConstructor; import lombok.Data; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; @@ -27,10 +27,12 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. * 1. 编码 Modbus 读请求帧 * 2. 通过 ConnectionManager 发送到设备的 TCP 连接 * 3. 将请求注册到 PendingRequestManager,等待设备响应 + *

                              + * 闭包只捕获 deviceId + pointId,运行时从 configCacheService 获取最新 config 和 point, + * 避免闭包捕获旧快照导致上报消息用旧身份的问题。 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public class IotModbusTcpSlavePollScheduler { @@ -38,7 +40,17 @@ public class IotModbusTcpSlavePollScheduler { private final IotModbusTcpSlaveConnectionManager connectionManager; private final IotModbusFrameEncoder frameEncoder; private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; + private final IotModbusTcpSlaveConfigCacheService configCacheService; private final int requestTimeout; + /** + * TCP 事务 ID 自增器(与 DownstreamHandler 共享) + */ + private final AtomicInteger transactionIdCounter; + + /** + * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积 + */ + private static final long MIN_REQUEST_INTERVAL = 200; /** * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) @@ -46,9 +58,33 @@ public class IotModbusTcpSlavePollScheduler { private final Map> devicePointTimers = new ConcurrentHashMap<>(); /** - * TCP 事务 ID 自增器 + * per-device 请求队列:deviceId -> 待执行请求队列 */ - private final AtomicInteger transactionIdCounter = new AtomicInteger(0); + private final Map> deviceRequestQueues = new ConcurrentHashMap<>(); + /** + * per-device 上次请求时间戳:deviceId -> lastRequestTimeMs + */ + private final Map deviceLastRequestTime = new ConcurrentHashMap<>(); + /** + * per-device 延迟 timer 标记:deviceId -> 是否有延迟 timer 在等待 + */ + private final Map deviceDelayTimerActive = new ConcurrentHashMap<>(); + + public IotModbusTcpSlavePollScheduler(Vertx vertx, + IotModbusTcpSlaveConnectionManager connectionManager, + IotModbusFrameEncoder frameEncoder, + IotModbusTcpSlavePendingRequestManager pendingRequestManager, + int requestTimeout, + AtomicInteger transactionIdCounter, + IotModbusTcpSlaveConfigCacheService configCacheService) { + this.vertx = vertx; + this.connectionManager = connectionManager; + this.frameEncoder = frameEncoder; + this.pendingRequestManager = pendingRequestManager; + this.requestTimeout = requestTimeout; + this.transactionIdCounter = transactionIdCounter; + this.configCacheService = configCacheService; + } /** * 点位定时器信息 @@ -68,8 +104,15 @@ public class IotModbusTcpSlavePollScheduler { } + // ========== 轮询管理 ========== + /** * 更新轮询任务(增量更新) + * + * 1. 【删除】点位:停止对应的轮询定时器 + * 2. 【新增】点位:创建对应的轮询定时器 + * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器 + * 4. 其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point) */ public void updatePolling(IotModbusDeviceConfigRespDTO config) { Long deviceId = config.getDeviceId(); @@ -101,7 +144,7 @@ public class IotModbusTcpSlavePollScheduler { PointTimerInfo existingTimer = currentTimers.get(pointId); // 3.1 新增点位:创建定时器 if (existingTimer == null) { - Long timerId = createPollTimer(config, point); + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); if (timerId != null) { currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", @@ -110,7 +153,7 @@ public class IotModbusTcpSlavePollScheduler { } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { // 3.2 pollInterval 变化:重建定时器 vertx.cancelTimer(existingTimer.getTimerId()); - Long timerId = createPollTimer(config, point); + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); if (timerId != null) { currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", @@ -119,54 +162,144 @@ public class IotModbusTcpSlavePollScheduler { currentTimers.remove(pointId); } } - // 3.3 其他属性变化:不处理(下次轮询时自动使用新配置) + // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置 } } /** * 创建轮询定时器 + *

                              + * 闭包只捕获 deviceId 和 pointId,运行时从 configCache 获取最新配置,避免旧快照问题。 */ - private Long createPollTimer(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { - if (point.getPollInterval() == null || point.getPollInterval() <= 0) { + private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) { + if (pollInterval == null || pollInterval <= 0) { return null; } - return vertx.setPeriodic(point.getPollInterval(), timerId -> { + return vertx.setPeriodic(pollInterval, timerId -> { try { - pollPoint(config, point); + submitPollRequest(deviceId, pointId); } catch (Exception e) { - log.error("[createPollTimer][轮询点位失败, deviceId={}, identifier={}]", - config.getDeviceId(), point.getIdentifier(), e); + log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e); } }); } + // ========== 请求队列(per-device 限速) ========== + /** - * 轮询单个点位:编码读请求帧 → 发送 → 注册 PendingRequest + * 提交轮询请求到设备请求队列(保证同设备请求间隔) */ - private void pollPoint(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { - Long deviceId = config.getDeviceId(); - // 1. 获取连接信息 + private void submitPollRequest(Long deviceId, Long pointId) { + // 1. 将请求添加到设备的请求队列 + Queue queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>()); + queue.offer(() -> pollPoint(deviceId, pointId)); + + // 2. 处理设备请求队列(如果没有延迟 timer 在等待) + processDeviceQueue(deviceId); + } + + /** + * 处理设备请求队列 + */ + private void processDeviceQueue(Long deviceId) { + Queue queue = deviceRequestQueues.get(deviceId); + if (CollUtil.isEmpty(queue)) { + return; + } + // 检查是否已有延迟 timer 在等待 + if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) { + return; + } + + // TODO @AI:可以改成不满足间隔,然后 return,简化括号层级; + long now = System.currentTimeMillis(); + long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L); + long elapsed = now - lastTime; + + if (elapsed >= MIN_REQUEST_INTERVAL) { + // 满足间隔要求,立即执行 + Runnable task = queue.poll(); + if (task != null) { + deviceLastRequestTime.put(deviceId, now); + task.run(); + // 继续处理队列中的下一个(如果有的话,需要延迟) + if (!queue.isEmpty()) { + scheduleNextRequest(deviceId); + } + } + } else { + // 需要延迟 + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed); + } + } + + private void scheduleNextRequest(Long deviceId) { + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL); + } + + private void scheduleNextRequest(Long deviceId, long delayMs) { + deviceDelayTimerActive.put(deviceId, true); + vertx.setTimer(delayMs, id -> { + deviceDelayTimerActive.put(deviceId, false); + Queue queue = deviceRequestQueues.get(deviceId); + // TODO @AI:if return?简化下? + if (CollUtil.isEmpty(queue)) { + Runnable task = queue.poll(); + if (task != null) { + deviceLastRequestTime.put(deviceId, System.currentTimeMillis()); + task.run(); + } + // 继续处理 + if (queue != null && !queue.isEmpty()) { + scheduleNextRequest(deviceId); + } + } + }); + } + + // ========== 轮询执行 ========== + + /** + * 轮询单个点位 + *

                              + * 运行时从 configCacheService 获取最新的 config 和 point,而非使用闭包捕获的旧引用。 + */ + private void pollPoint(Long deviceId, Long pointId) { + // 1.1 从 configCache 获取最新配置 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); + if (config == null || CollUtil.isEmpty(config.getPoints())) { + log.warn("[pollPoint][设备 {} 没有配置]", deviceId); + return; + } + // 1.2 查找点位 + IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId)); + if (point == null) { + log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId); + return; + } + + // 2. 获取连接信息 ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(deviceId); if (connInfo == null) { log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId); return; } - // 2.1 确定帧格式和事务 ID + // 3.1 确定帧格式和事务 ID IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); if (frameFormat == null) { log.warn("[pollPoint][设备 {} 帧格式为空,跳过轮询]", deviceId); return; } - // TODO @AI:是不是得按照设备递增? Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP ? (transactionIdCounter.incrementAndGet() & 0xFFFF) : null; + // TODO @AI:这里断言必须非空! int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; - // 2.2 编码读请求 + // 3.2 编码读请求 byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(), point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId); - // 2.3 注册 PendingRequest + // 3.3 注册 PendingRequest PendingRequest pendingRequest = new PendingRequest( deviceId, point.getId(), point.getIdentifier(), slaveId, point.getFunctionCode(), @@ -175,13 +308,15 @@ public class IotModbusTcpSlavePollScheduler { System.currentTimeMillis() + requestTimeout); pendingRequestManager.addRequest(pendingRequest); - // 3. 发送读请求 + // 4. 发送读请求 connectionManager.sendToDevice(deviceId, data); log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]", deviceId, point.getIdentifier(), point.getFunctionCode(), point.getRegisterAddress(), point.getRegisterCount()); } + // ========== 停止 ========== + /** * 停止设备的轮询 */ @@ -193,6 +328,10 @@ public class IotModbusTcpSlavePollScheduler { for (PointTimerInfo timerInfo : timers.values()) { vertx.cancelTimer(timerInfo.getTimerId()); } + // 清理请求队列 + deviceRequestQueues.remove(deviceId); + deviceLastRequestTime.remove(deviceId); + deviceDelayTimerActive.remove(deviceId); log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java index c6c7ff28c3..c81ab4a83a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java @@ -25,6 +25,7 @@ import java.nio.ByteOrder; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +// TODO @芋艿:【晚点改】单测需要简化! /** * IoT Modbus TCP Slave 协议集成测试 — MODBUS_TCP 帧格式(手动测试) * @@ -123,6 +124,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { // 1. 先认证 IotModbusFrame authResponse = authenticate(socket); log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); + // TODO @AI:这里断言下,认证必须成功! // 2. 设置持续监听:每收到一个读请求,自动回复 log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); From c608b81c4e9e347d42a56a0847cf94de449ee4f7 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 8 Feb 2026 16:34:22 +0800 Subject: [PATCH 38/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9Amodbus-?= =?UTF-8?q?tcp-slave=20=E4=BC=98=E5=8C=96=E5=A4=A7=E9=87=8F=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=EF=BC=8C=E4=B8=BB=E8=A6=81=E6=98=AF=20polling=20?= =?UTF-8?q?=E8=BD=AE=E8=AF=A2=E7=9A=84=E5=A4=8D=E7=94=A8=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...tractIotProtocolDownstreamSubscriber.java} | 2 +- .../IotCoapDownstreamSubscriber.java | 4 +- .../IotEmqxDownstreamSubscriber.java | 4 +- .../IotHttpDownstreamSubscriber.java | 4 +- .../AbstractIotModbusPollScheduler.java | 269 +++++++++++++++++ .../IotModbusCommonUtils.java} | 15 +- .../utils/IotModbusTcpMasterUtils.java} | 6 +- .../IotModbusTcpDownstreamHandler.java | 12 +- .../IotModbusTcpDownstreamSubscriber.java | 4 +- .../upstream/IotModbusTcpUpstreamHandler.java | 4 +- .../manager/IotModbusTcpPollScheduler.java | 252 +--------------- .../modbus/tcpslave/codec/IotModbusFrame.java | 4 +- .../tcpslave/codec/IotModbusFrameDecoder.java | 20 +- .../tcpslave/codec/IotModbusFrameEncoder.java | 8 +- .../IotModbusTcpSlaveDownstreamHandler.java | 12 +- ...IotModbusTcpSlaveDownstreamSubscriber.java | 4 +- .../IotModbusTcpSlaveUpstreamHandler.java | 9 +- .../IotModbusTcpSlaveConnectionManager.java | 4 +- .../IotModbusTcpSlavePollScheduler.java | 282 ++---------------- .../IotMqttDownstreamSubscriber.java | 4 +- .../IotTcpDownstreamSubscriber.java | 4 +- .../IotUdpDownstreamSubscriber.java | 4 +- .../IotWebSocketDownstreamSubscriber.java | 4 +- ...odbusTcpSlaveModbusRtuIntegrationTest.java | 4 +- 24 files changed, 373 insertions(+), 566 deletions(-) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{IotProtocolDownstreamSubscriber.java => AbstractIotProtocolDownstreamSubscriber.java} (95%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/{IotModbusUtils.java => utils/IotModbusCommonUtils.java} (97%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpmaster/client/IotModbusTcpClientUtils.java => common/utils/IotModbusTcpMasterUtils.java} (98%) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/AbstractIotProtocolDownstreamSubscriber.java similarity index 95% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/AbstractIotProtocolDownstreamSubscriber.java index 2e2150f6f7..efd61e13a2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java +++ b/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; */ @AllArgsConstructor @Slf4j -public abstract class IotProtocolDownstreamSubscriber implements IotMessageSubscriber { +public abstract class AbstractIotProtocolDownstreamSubscriber implements IotMessageSubscriber { private final IotProtocol protocol; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java index 188d2e6428..3309d2cd49 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java +++ b/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; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; import lombok.extern.slf4j.Slf4j; @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber { +public class IotCoapDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) { super(protocol, messageBus); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java index 55aaaac69c..e7e5de98db 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java +++ b/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; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import lombok.extern.slf4j.Slf4j; @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber { +public class IotEmqxDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { private final IotEmqxDownstreamHandler downstreamHandler; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java index bfac16ca5e..fe94fe6172 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java +++ b/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; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; import lombok.extern.slf4j.Slf4j; /** @@ -13,7 +13,7 @@ import lombok.extern.slf4j.Slf4j; */ @Slf4j -public class IotHttpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { +public class IotHttpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) { super(protocol, messageBus); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java new file mode 100644 index 0000000000..dedac7acd3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java @@ -0,0 +1,269 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import io.vertx.core.Vertx; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * Modbus 轮询调度器基类 + *

                              + * 封装通用的定时器管理、per-device 请求队列限速逻辑。 + * 子类只需实现 {@link #pollPoint(Long, Long)} 定义具体的轮询动作。 + *

                              + * + * @author 芋道源码 + */ +@Slf4j +public abstract class AbstractIotModbusPollScheduler { + + protected final Vertx vertx; + + /** + * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积 + */ + private static final long MIN_REQUEST_INTERVAL = 1000; + + /** + * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) + */ + private final Map> devicePointTimers = new ConcurrentHashMap<>(); + + /** + * per-device 请求队列:deviceId -> 待执行请求队列 + */ + private final Map> deviceRequestQueues = new ConcurrentHashMap<>(); + /** + * per-device 上次请求时间戳:deviceId -> lastRequestTimeMs + */ + private final Map deviceLastRequestTime = new ConcurrentHashMap<>(); + /** + * per-device 延迟 timer 标记:deviceId -> 是否有延迟 timer 在等待 + */ + private final Map deviceDelayTimerActive = new ConcurrentHashMap<>(); + + protected AbstractIotModbusPollScheduler(Vertx vertx) { + this.vertx = vertx; + } + + /** + * 点位定时器信息 + */ + @Data + @AllArgsConstructor + private static class PointTimerInfo { + + /** + * Vert.x 定时器 ID + */ + private Long timerId; + /** + * 轮询间隔(用于判断是否需要更新定时器) + */ + private Integer pollInterval; + + } + + // ========== 轮询管理 ========== + + /** + * 更新轮询任务(增量更新) + * + * 1. 【删除】点位:停止对应的轮询定时器 + * 2. 【新增】点位:创建对应的轮询定时器 + * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器 + * 【修改】其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point) + */ + public void updatePolling(IotModbusDeviceConfigRespDTO config) { + Long deviceId = config.getDeviceId(); + List newPoints = config.getPoints(); + Map currentTimers = devicePointTimers + .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>()); + // 1.1 计算新配置中的点位 ID 集合 + Set newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId); + // 1.2 计算删除的点位 ID 集合 + Set removedPointIds = new HashSet<>(currentTimers.keySet()); + removedPointIds.removeAll(newPointIds); + + // 2. 处理删除的点位:停止不再存在的定时器 + for (Long pointId : removedPointIds) { + PointTimerInfo timerInfo = currentTimers.remove(pointId); + if (timerInfo != null) { + vertx.cancelTimer(timerInfo.getTimerId()); + log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId); + } + } + + // 3. 处理新增和修改的点位 + if (CollUtil.isEmpty(newPoints)) { + return; + } + for (IotModbusPointRespDTO point : newPoints) { + Long pointId = point.getId(); + Integer newPollInterval = point.getPollInterval(); + PointTimerInfo existingTimer = currentTimers.get(pointId); + // 3.1 新增点位:创建定时器 + if (existingTimer == null) { + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); + if (timerId != null) { + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); + log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", + deviceId, pointId, newPollInterval); + } + } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { + // 3.2 pollInterval 变化:重建定时器 + vertx.cancelTimer(existingTimer.getTimerId()); + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); + if (timerId != null) { + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); + log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", + deviceId, pointId, existingTimer.getPollInterval(), newPollInterval); + } else { + currentTimers.remove(pointId); + } + } + // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置 + } + } + + /** + * 创建轮询定时器 + */ + private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) { + if (pollInterval == null || pollInterval <= 0) { + return null; + } + return vertx.setPeriodic(pollInterval, timerId -> { + try { + submitPollRequest(deviceId, pointId); + } catch (Exception e) { + log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e); + } + }); + } + + // ========== 请求队列(per-device 限速) ========== + + /** + * 提交轮询请求到设备请求队列(保证同设备请求间隔) + */ + private void submitPollRequest(Long deviceId, Long pointId) { + // 1. 【重要】将请求添加到设备的请求队列 + Queue queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>()); + queue.offer(() -> pollPoint(deviceId, pointId)); + + // 2. 处理设备请求队列(如果没有延迟 timer 在等待) + processDeviceQueue(deviceId); + } + + /** + * 处理设备请求队列 + */ + private void processDeviceQueue(Long deviceId) { + Queue queue = deviceRequestQueues.get(deviceId); + if (CollUtil.isEmpty(queue)) { + return; + } + // 检查是否已有延迟 timer 在等待 + if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) { + return; + } + + // 不满足间隔要求,延迟执行 + long now = System.currentTimeMillis(); + long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L); + long elapsed = now - lastTime; + if (elapsed < MIN_REQUEST_INTERVAL) { + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed); + return; + } + + // 满足间隔要求,立即执行 + Runnable task = queue.poll(); + if (task == null) { + return; + } + deviceLastRequestTime.put(deviceId, now); + task.run(); + // 继续处理队列中的下一个(如果有的话,需要延迟) + if (CollUtil.isNotEmpty(queue)) { + scheduleNextRequest(deviceId); + } + } + + private void scheduleNextRequest(Long deviceId) { + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL); + } + + private void scheduleNextRequest(Long deviceId, long delayMs) { + deviceDelayTimerActive.put(deviceId, true); + vertx.setTimer(delayMs, id -> { + deviceDelayTimerActive.put(deviceId, false); + Queue queue = deviceRequestQueues.get(deviceId); + if (CollUtil.isEmpty(queue)) { + return; + } + + // 满足间隔要求,立即执行 + Runnable task = queue.poll(); + if (task == null) { + return; + } + deviceLastRequestTime.put(deviceId, System.currentTimeMillis()); + task.run(); + // 继续处理队列中的下一个(如果有的话,需要延迟) + if (CollUtil.isNotEmpty(queue)) { + scheduleNextRequest(deviceId); + } + }); + } + + // ========== 轮询执行 ========== + + /** + * 轮询单个点位(子类实现具体的读取逻辑) + * + * @param deviceId 设备 ID + * @param pointId 点位 ID + */ + protected abstract void pollPoint(Long deviceId, Long pointId); + + // ========== 停止 ========== + + /** + * 停止设备的轮询 + */ + public void stopPolling(Long deviceId) { + Map timers = devicePointTimers.remove(deviceId); + if (CollUtil.isEmpty(timers)) { + return; + } + for (PointTimerInfo timerInfo : timers.values()) { + vertx.cancelTimer(timerInfo.getTimerId()); + } + // 清理请求队列 + deviceRequestQueues.remove(deviceId); + deviceLastRequestTime.remove(deviceId); + deviceDelayTimerActive.remove(deviceId); + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); + } + + /** + * 停止所有轮询 + */ + public void stopAll() { + for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) { + stopPolling(deviceId); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java similarity index 97% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java index 227a886f8b..236277bc0a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; @@ -34,7 +34,7 @@ import java.nio.ByteOrder; */ @UtilityClass @Slf4j -public class IotModbusUtils { +public class IotModbusCommonUtils { /** FC01: 读线圈 */ public static final int FC_READ_COILS = 1; @@ -512,4 +512,15 @@ public class IotModbusUtils { return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); } + /** + * 根据点位 ID 查找点位配置 + * + * @param config 设备 Modbus 配置 + * @param pointId 点位 ID + * @return 匹配的点位配置,未找到返回 null + */ + public static IotModbusPointRespDTO findPointById(IotModbusDeviceConfigRespDTO config, Long pointId) { + return CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId)); + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClientUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java similarity index 98% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClientUtils.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java index 44728396ac..5804ea6022 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClientUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; @@ -12,7 +12,7 @@ import io.vertx.core.Future; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; -import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils.*; +import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.*; /** * IoT Modbus TCP 客户端工具类 @@ -26,7 +26,7 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModb */ @UtilityClass @Slf4j -public class IotModbusTcpClientUtils { +public class IotModbusTcpMasterUtils { /** * 读取 Modbus 数据 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java index 27df39eefa..32dfde5a64 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java @@ -4,8 +4,8 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClientUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpMasterUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; import lombok.RequiredArgsConstructor; @@ -57,13 +57,13 @@ public class IotModbusTcpDownstreamHandler { String identifier = entry.getKey(); Object value = entry.getValue(); // 2.1 查找对应的点位配置 - IotModbusPointRespDTO point = IotModbusUtils.findPoint(config, identifier); + IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier); if (point == null) { log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); continue; } // 2.2 检查是否支持写操作 - if (!IotModbusUtils.isWritable(point.getFunctionCode())) { + if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) { log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); continue; } @@ -91,9 +91,9 @@ public class IotModbusTcpDownstreamHandler { } // 2.1 转换属性值为原始值 - int[] rawValues = IotModbusUtils.convertToRawValues(value, point); + int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point); // 2.2 执行 Modbus 写入 - IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues) + IotModbusTcpMasterUtils.write(connection, slaveId, point, rawValues) .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", config.getDeviceId(), point.getIdentifier(), value)) .onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]", diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java index c8e22aff79..74afcbb38f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.do import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol; import lombok.extern.slf4j.Slf4j; @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotModbusTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { +public class IotModbusTcpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { private final IotModbusTcpDownstreamHandler downstreamHandler; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java index 8762481ebc..de02af06c2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java @@ -5,7 +5,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import lombok.extern.slf4j.Slf4j; @@ -40,7 +40,7 @@ public class IotModbusTcpUpstreamHandler { int[] rawValue) { try { // 1.1 转换原始值为物模型属性值 - Object convertedValue = IotModbusUtils.convertToPropertyValue(rawValue, point); + Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValue, point); log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]", config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue); // 1.2 构造属性上报消息 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java index afe4a9caed..2f049561c9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java @@ -1,242 +1,45 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClientUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpMasterUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler; import io.vertx.core.Vertx; -import lombok.AllArgsConstructor; -import lombok.Data; import lombok.extern.slf4j.Slf4j; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; - -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; - -// TODO @AI:类的命名上,要体现上 master。其它类似 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster 也要! /** * IoT Modbus TCP Master 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 * * @author 芋道源码 */ @Slf4j -public class IotModbusTcpPollScheduler { +public class IotModbusTcpPollScheduler extends AbstractIotModbusPollScheduler { - private final Vertx vertx; private final IotModbusTcpConnectionManager connectionManager; private final IotModbusTcpUpstreamHandler upstreamHandler; private final IotModbusTcpConfigCacheService configCacheService; - /** - * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积 - */ - private static final long MIN_REQUEST_INTERVAL = 100; - - /** - * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) - */ - private final Map> devicePointTimers = new ConcurrentHashMap<>(); - - /** - * per-device 请求队列:deviceId -> 待执行请求队列 - */ - private final Map> deviceRequestQueues = new ConcurrentHashMap<>(); - - /** - * per-device 上次请求时间戳:deviceId -> lastRequestTimeMs - */ - private final Map deviceLastRequestTime = new ConcurrentHashMap<>(); - - /** - * per-device 延迟 timer 标记:deviceId -> 是否有延迟 timer 在等待 - */ - private final Map deviceDelayTimerActive = new ConcurrentHashMap<>(); - public IotModbusTcpPollScheduler(Vertx vertx, IotModbusTcpConnectionManager connectionManager, IotModbusTcpUpstreamHandler upstreamHandler, IotModbusTcpConfigCacheService configCacheService) { - this.vertx = vertx; + super(vertx); this.connectionManager = connectionManager; this.upstreamHandler = upstreamHandler; this.configCacheService = configCacheService; } - // ========== 点位定时器 ========== - - /** - * 点位定时器信息 - */ - @Data - @AllArgsConstructor - private static class PointTimerInfo { - - /** - * Vert.x 定时器 ID - */ - private Long timerId; - /** - * 轮询间隔(用于判断是否需要更新定时器) - */ - private Integer pollInterval; - - } - - // ========== 轮询管理 ========== - - /** - * 更新轮询任务(增量更新) - * - * 1. 【删除】点位:停止对应的轮询定时器 - * 2. 【新增】点位:创建对应的轮询定时器 - * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器 - * 4. 其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point) - */ - public void updatePolling(IotModbusDeviceConfigRespDTO config) { - Long deviceId = config.getDeviceId(); - List newPoints = config.getPoints(); - Map currentTimers = devicePointTimers - .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>()); - // 1.1 计算新配置中的点位 ID 集合 - Set newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId); - // 1.2 计算删除的点位 ID 集合 - Set removedPointIds = new HashSet<>(currentTimers.keySet()); - removedPointIds.removeAll(newPointIds); - - // 2. 处理删除的点位:停止不再存在的定时器 - for (Long pointId : removedPointIds) { - PointTimerInfo timerInfo = currentTimers.remove(pointId); - if (timerInfo != null) { - vertx.cancelTimer(timerInfo.getTimerId()); - log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId); - } - } - - // 3. 处理新增和修改的点位 - if (CollUtil.isEmpty(newPoints)) { - return; - } - for (IotModbusPointRespDTO point : newPoints) { - Long pointId = point.getId(); - Integer newPollInterval = point.getPollInterval(); - PointTimerInfo existingTimer = currentTimers.get(pointId); - // 3.1 新增点位:创建定时器 - if (existingTimer == null) { - Long timerId = createPollTimer(deviceId, pointId, newPollInterval); - if (timerId != null) { - currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); - log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", - deviceId, pointId, newPollInterval); - } - } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { - // 3.2 pollInterval 变化:重建定时器 - vertx.cancelTimer(existingTimer.getTimerId()); - Long timerId = createPollTimer(deviceId, pointId, newPollInterval); - if (timerId != null) { - currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); - log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", - deviceId, pointId, existingTimer.getPollInterval(), newPollInterval); - } else { - currentTimers.remove(pointId); - } - } - // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置 - } - } - - /** - * 创建轮询定时器 - *

                              - * 闭包只捕获 deviceId 和 pointId,运行时从 configCache 获取最新配置,避免旧快照问题。 - */ - private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) { - if (pollInterval == null || pollInterval <= 0) { - return null; - } - return vertx.setPeriodic(pollInterval, timerId -> { - try { - submitPollRequest(deviceId, pointId); - } catch (Exception e) { - log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e); - } - }); - } - - // ========== 请求队列(per-device 限速) ========== - - /** - * 提交轮询请求到设备请求队列(保证同设备请求间隔) - */ - private void submitPollRequest(Long deviceId, Long pointId) { - Queue queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>()); - queue.offer(() -> pollPoint(deviceId, pointId)); - processDeviceQueue(deviceId); - } - - /** - * 处理设备请求队列 - */ - private void processDeviceQueue(Long deviceId) { - Queue queue = deviceRequestQueues.get(deviceId); - if (CollUtil.isEmpty(queue)) { - return; - } - // 检查是否已有延迟 timer 在等待 - if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) { - return; - } - long now = System.currentTimeMillis(); - long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L); - long elapsed = now - lastTime; - if (elapsed >= MIN_REQUEST_INTERVAL) { - // 满足间隔要求,立即执行 - Runnable task = queue.poll(); - if (task != null) { - deviceLastRequestTime.put(deviceId, now); - task.run(); - // 继续处理队列中的下一个(如果有的话,需要延迟) - if (!queue.isEmpty()) { - scheduleNextRequest(deviceId); - } - } - } else { - // 需要延迟 - scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed); - } - } - - private void scheduleNextRequest(Long deviceId) { - scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL); - } - - private void scheduleNextRequest(Long deviceId, long delayMs) { - deviceDelayTimerActive.put(deviceId, true); - vertx.setTimer(delayMs, id -> { - deviceDelayTimerActive.put(deviceId, false); - Queue queue = deviceRequestQueues.get(deviceId); - if (CollUtil.isEmpty(queue)) { - Runnable task = queue.poll(); - if (task != null) { - deviceLastRequestTime.put(deviceId, System.currentTimeMillis()); - task.run(); - } - // 继续处理 - if (queue != null && !queue.isEmpty()) { - scheduleNextRequest(deviceId); - } - } - }); - } - // ========== 轮询执行 ========== /** * 轮询单个点位 */ - private void pollPoint(Long deviceId, Long pointId) { + @Override + protected void pollPoint(Long deviceId, Long pointId) { // 1.1 从 configCache 获取最新配置 IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); if (config == null || CollUtil.isEmpty(config.getPoints())) { @@ -244,8 +47,7 @@ public class IotModbusTcpPollScheduler { return; } // 1.2 查找点位 - // TODO @AI:是不是这里,可以抽到 IotModbusUtils 里?感觉应该有几个地方需要的; - IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId)); + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId); if (point == null) { log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId); return; @@ -259,45 +61,13 @@ public class IotModbusTcpPollScheduler { } // 2.2 获取 slave ID Integer slaveId = connectionManager.getSlaveId(deviceId); - if (slaveId == null) { - log.warn("[pollPoint][设备 {} 没有 slaveId]", deviceId); - return; - } + Assert.notNull(slaveId, "设备 {} 没有配置 slaveId", deviceId); // 3. 执行 Modbus 读取 - IotModbusTcpClientUtils.read(connection, slaveId, point) + IotModbusTcpMasterUtils.read(connection, slaveId, point) .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue)) .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]", deviceId, point.getIdentifier(), e)); } - // ========== 停止 ========== - - /** - * 停止设备的轮询 - */ - public void stopPolling(Long deviceId) { - Map timers = devicePointTimers.remove(deviceId); - if (CollUtil.isEmpty(timers)) { - return; - } - for (PointTimerInfo timerInfo : timers.values()) { - vertx.cancelTimer(timerInfo.getTimerId()); - } - // 清理请求队列 - deviceRequestQueues.remove(deviceId); - deviceLastRequestTime.remove(deviceId); - deviceDelayTimerActive.remove(deviceId); - log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); - } - - /** - * 停止所有轮询 - */ - public void stopAll() { - for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) { - stopPolling(deviceId); - } - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java index 6ac6930337..b7661abcfc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import lombok.Data; import lombok.experimental.Accessors; @@ -39,7 +39,7 @@ public class IotModbusFrame { * 当功能码最高位为 1 时(异常响应),此字段存储异常码。 * 为 null 表示非异常响应。 * - * @see IotModbusUtils#FC_EXCEPTION_MASK + * @see IotModbusCommonUtils#FC_EXCEPTION_MASK */ private Integer exceptionCode; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java index 09d98e0dba..fc5219e197 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.parsetools.RecordParser; @@ -96,7 +96,7 @@ public class IotModbusFrameDecoder { return null; } // 校验 CRC - if (!IotModbusUtils.verifyCrc16(data)) { + if (!IotModbusCommonUtils.verifyCrc16(data)) { log.warn("[decodeRtuResponse][CRC 校验失败]"); return null; } @@ -119,8 +119,8 @@ public class IotModbusFrameDecoder { .setPdu(pdu) .setTransactionId(transactionId); // 异常响应 - if (IotModbusUtils.isExceptionResponse(functionCode)) { - frame.setFunctionCode(IotModbusUtils.extractOriginalFunctionCode(functionCode)); + if (IotModbusCommonUtils.isExceptionResponse(functionCode)) { + frame.setFunctionCode(IotModbusCommonUtils.extractOriginalFunctionCode(functionCode)); if (pdu.length >= 1) { frame.setExceptionCode(pdu[0] & 0xFF); } @@ -281,7 +281,7 @@ public class IotModbusFrameDecoder { this.slaveId = bytes[0]; this.functionCode = bytes[1]; int fc = functionCode & 0xFF; - if (IotModbusUtils.isExceptionResponse(fc)) { + if (IotModbusCommonUtils.isExceptionResponse(fc)) { // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节 // 已有 6 字节(多 1 字节),取前 5 字节组装 Buffer frame = Buffer.buffer(5); @@ -290,7 +290,7 @@ public class IotModbusFrameDecoder { frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC emitFrame(frame); resetToHeader(); - } else if (IotModbusUtils.isReadResponse(fc) || fc == customFunctionCode) { + } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) { // 读响应或自定义 FC:bytes[2] = byteCount this.byteCount = bytes[2]; int bc = byteCount & 0xFF; @@ -315,7 +315,7 @@ public class IotModbusFrameDecoder { this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC parser.fixedSizeMode(remaining); } - } else if (IotModbusUtils.isWriteResponse(fc)) { + } else if (IotModbusCommonUtils.isWriteResponse(fc)) { // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节 // 已有 6 字节,还需 2 字节 state = STATE_WRITE_BODY; @@ -356,15 +356,15 @@ public class IotModbusFrameDecoder { this.slaveId = header[0]; this.functionCode = header[1]; int fc = functionCode & 0xFF; - if (IotModbusUtils.isExceptionResponse(fc)) { + if (IotModbusCommonUtils.isExceptionResponse(fc)) { // 异常响应 state = STATE_EXCEPTION_BODY; parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2) - } else if (IotModbusUtils.isReadResponse(fc) || fc == customFunctionCode) { + } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) { // 读响应或自定义 FC state = STATE_READ_BYTE_COUNT; parser.fixedSizeMode(1); // byteCount - } else if (IotModbusUtils.isWriteResponse(fc)) { + } else if (IotModbusCommonUtils.isWriteResponse(fc)) { // 写响应 state = STATE_WRITE_BODY; pendingData = Buffer.buffer(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java index 195c82352a..36323cbda5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -60,7 +60,7 @@ public class IotModbusFrameEncoder { public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value, IotModbusFrameFormatEnum format, Integer transactionId) { // FC05 单写线圈:Modbus 标准要求 value 为 0xFF00(ON)或 0x0000(OFF) - if (functionCode == IotModbusUtils.FC_WRITE_SINGLE_COIL) { + if (functionCode == IotModbusCommonUtils.FC_WRITE_SINGLE_COIL) { value = (value != 0) ? 0xFF00 : 0x0000; } // PDU: [FC(1)] [Address(2)] [Value(2)] @@ -120,7 +120,7 @@ public class IotModbusFrameEncoder { int quantity = values.length; int byteCount = (quantity + 7) / 8; // 向上取整 byte[] pdu = new byte[6 + byteCount]; - pdu[0] = (byte) IotModbusUtils.FC_WRITE_MULTIPLE_COILS; // FC15 + pdu[0] = (byte) IotModbusCommonUtils.FC_WRITE_MULTIPLE_COILS; // FC15 pdu[1] = (byte) ((address >> 8) & 0xFF); pdu[2] = (byte) (address & 0xFF); pdu[3] = (byte) ((quantity >> 8) & 0xFF); @@ -204,7 +204,7 @@ public class IotModbusFrameEncoder { frame[0] = (byte) slaveId; System.arraycopy(pdu, 0, frame, 1, pdu.length); // 计算并追加 CRC16 - int crc = IotModbusUtils.calculateCrc16(frame, frame.length - 2); + int crc = IotModbusCommonUtils.calculateCrc16(frame, frame.length - 2); frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High return frame; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java index ca9edde53e..770f13e3f8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java @@ -7,7 +7,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; @@ -82,13 +82,13 @@ public class IotModbusTcpSlaveDownstreamHandler { String identifier = entry.getKey(); Object value = entry.getValue(); // 2.1 查找对应的点位配置 - IotModbusPointRespDTO point = IotModbusUtils.findPoint(config, identifier); + IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier); if (point == null) { log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); continue; } // 2.2 检查是否支持写操作 - if (!IotModbusUtils.isWritable(point.getFunctionCode())) { + if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) { log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); continue; } @@ -104,7 +104,7 @@ public class IotModbusTcpSlaveDownstreamHandler { private void writeProperty(Long deviceId, ConnectionInfo connInfo, IotModbusPointRespDTO point, Object value) { // 1.1 转换属性值为原始值 - int[] rawValues = IotModbusUtils.convertToRawValues(value, point); + int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point); // 1.2 确定帧格式和事务 ID IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); @@ -117,8 +117,8 @@ public class IotModbusTcpSlaveDownstreamHandler { // 1.3 编码写请求 byte[] data; int readFunctionCode = point.getFunctionCode(); - Integer writeSingleCode = IotModbusUtils.getWriteSingleFunctionCode(readFunctionCode); - Integer writeMultipleCode = IotModbusUtils.getWriteMultipleFunctionCode(readFunctionCode); + Integer writeSingleCode = IotModbusCommonUtils.getWriteSingleFunctionCode(readFunctionCode); + Integer writeMultipleCode = IotModbusCommonUtils.getWriteMultipleFunctionCode(readFunctionCode); if (rawValues.length == 1 && writeSingleCode != null) { // 单个值:使用单写功能码(FC05/FC06) data = frameEncoder.encodeWriteSingleRequest(slaveId, writeSingleCode, diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java index 4e7b882770..2a11cff565 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.dow import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol; import lombok.extern.slf4j.Slf4j; @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotModbusTcpSlaveDownstreamSubscriber extends IotProtocolDownstreamSubscriber { +public class IotModbusTcpSlaveDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { private final IotModbusTcpSlaveDownstreamHandler downstreamHandler; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java index 67b2d52e8f..6ede74c1db 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java @@ -19,7 +19,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; @@ -237,7 +237,7 @@ public class IotModbusTcpSlaveUpstreamHandler { return; } // 2.2 提取寄存器值 - int[] rawValues = IotModbusUtils.extractValues(frame); + int[] rawValues = IotModbusCommonUtils.extractValues(frame); if (rawValues == null) { log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]", info.getDeviceId(), request.getIdentifier()); @@ -248,14 +248,13 @@ public class IotModbusTcpSlaveUpstreamHandler { if (config == null || CollUtil.isEmpty(config.getPoints())) { return; } - IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(), - p -> p.getId().equals(request.getPointId())); + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, request.getPointId()); if (point == null) { return; } // 3.1 点位翻译 - Object convertedValue = IotModbusUtils.convertToPropertyValue(rawValues, point); + Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValues, point); // 3.2 上报属性 Map params = MapUtil.of(request.getIdentifier(), convertedValue); IotDeviceMessage message = IotDeviceMessage.requestOf( diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java index 241d8d777e..1a6a4cc610 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java @@ -75,8 +75,8 @@ public class IotModbusTcpSlaveConnectionManager { try { oldSocket.close(); } catch (Exception e) { - // TODO @AI:这里日志可以打的更完整一点,方便追溯;比如:设备 ID、旧连接地址等 - log.warn("[registerConnection][关闭旧 socket 失败]", e); + log.warn("[registerConnection][关闭旧 socket 失败, deviceId={}, oldRemote={}]", + info.getDeviceId(), oldSocket.remoteAddress(), e); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java index d0f38e3653..8a9cba5963 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java @@ -1,42 +1,28 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; import io.vertx.core.Vertx; -import lombok.AllArgsConstructor; -import lombok.Data; import lombok.extern.slf4j.Slf4j; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; - /** - * IoT Modbus TCP Slave 轮询调度器 - *

                              - * 管理点位的轮询定时器,为云端轮询模式的设备调度读取任务。 - * 与 tcpmaster 的 {@code IotModbusTcpPollScheduler} 不同,这里不通过 j2mod 直接读取,而是: - * 1. 编码 Modbus 读请求帧 - * 2. 通过 ConnectionManager 发送到设备的 TCP 连接 - * 3. 将请求注册到 PendingRequestManager,等待设备响应 - *

                              - * 闭包只捕获 deviceId + pointId,运行时从 configCacheService 获取最新 config 和 point, - * 避免闭包捕获旧快照导致上报消息用旧身份的问题。 + * IoT Modbus TCP Slave 轮询调度器:编码读请求帧,通过 TCP 连接发送到设备,注册 PendingRequest 等待响应 * * @author 芋道源码 */ @Slf4j -public class IotModbusTcpSlavePollScheduler { +public class IotModbusTcpSlavePollScheduler extends AbstractIotModbusPollScheduler { - private final Vertx vertx; private final IotModbusTcpSlaveConnectionManager connectionManager; private final IotModbusFrameEncoder frameEncoder; private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; @@ -47,29 +33,6 @@ public class IotModbusTcpSlavePollScheduler { */ private final AtomicInteger transactionIdCounter; - /** - * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积 - */ - private static final long MIN_REQUEST_INTERVAL = 200; - - /** - * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) - */ - private final Map> devicePointTimers = new ConcurrentHashMap<>(); - - /** - * per-device 请求队列:deviceId -> 待执行请求队列 - */ - private final Map> deviceRequestQueues = new ConcurrentHashMap<>(); - /** - * per-device 上次请求时间戳:deviceId -> lastRequestTimeMs - */ - private final Map deviceLastRequestTime = new ConcurrentHashMap<>(); - /** - * per-device 延迟 timer 标记:deviceId -> 是否有延迟 timer 在等待 - */ - private final Map deviceDelayTimerActive = new ConcurrentHashMap<>(); - public IotModbusTcpSlavePollScheduler(Vertx vertx, IotModbusTcpSlaveConnectionManager connectionManager, IotModbusFrameEncoder frameEncoder, @@ -77,7 +40,7 @@ public class IotModbusTcpSlavePollScheduler { int requestTimeout, AtomicInteger transactionIdCounter, IotModbusTcpSlaveConfigCacheService configCacheService) { - this.vertx = vertx; + super(vertx); this.connectionManager = connectionManager; this.frameEncoder = frameEncoder; this.pendingRequestManager = pendingRequestManager; @@ -86,185 +49,13 @@ public class IotModbusTcpSlavePollScheduler { this.configCacheService = configCacheService; } - /** - * 点位定时器信息 - */ - @Data - @AllArgsConstructor - private static class PointTimerInfo { - - /** - * Vert.x 定时器 ID - */ - private Long timerId; - /** - * 轮询间隔(用于判断是否需要更新定时器) - */ - private Integer pollInterval; - - } - - // ========== 轮询管理 ========== - - /** - * 更新轮询任务(增量更新) - * - * 1. 【删除】点位:停止对应的轮询定时器 - * 2. 【新增】点位:创建对应的轮询定时器 - * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器 - * 4. 其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point) - */ - public void updatePolling(IotModbusDeviceConfigRespDTO config) { - Long deviceId = config.getDeviceId(); - List newPoints = config.getPoints(); - Map currentTimers = devicePointTimers - .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>()); - // 1.1 计算新配置中的点位 ID 集合 - Set newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId); - // 1.2 计算删除的点位 ID 集合 - Set removedPointIds = new HashSet<>(currentTimers.keySet()); - removedPointIds.removeAll(newPointIds); - - // 2. 处理删除的点位:停止不再存在的定时器 - for (Long pointId : removedPointIds) { - PointTimerInfo timerInfo = currentTimers.remove(pointId); - if (timerInfo != null) { - vertx.cancelTimer(timerInfo.getTimerId()); - log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId); - } - } - - // 3. 处理新增和修改的点位 - if (CollUtil.isEmpty(newPoints)) { - return; - } - for (IotModbusPointRespDTO point : newPoints) { - Long pointId = point.getId(); - Integer newPollInterval = point.getPollInterval(); - PointTimerInfo existingTimer = currentTimers.get(pointId); - // 3.1 新增点位:创建定时器 - if (existingTimer == null) { - Long timerId = createPollTimer(deviceId, pointId, newPollInterval); - if (timerId != null) { - currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); - log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", - deviceId, pointId, newPollInterval); - } - } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { - // 3.2 pollInterval 变化:重建定时器 - vertx.cancelTimer(existingTimer.getTimerId()); - Long timerId = createPollTimer(deviceId, pointId, newPollInterval); - if (timerId != null) { - currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); - log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", - deviceId, pointId, existingTimer.getPollInterval(), newPollInterval); - } else { - currentTimers.remove(pointId); - } - } - // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置 - } - } - - /** - * 创建轮询定时器 - *

                              - * 闭包只捕获 deviceId 和 pointId,运行时从 configCache 获取最新配置,避免旧快照问题。 - */ - private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) { - if (pollInterval == null || pollInterval <= 0) { - return null; - } - return vertx.setPeriodic(pollInterval, timerId -> { - try { - submitPollRequest(deviceId, pointId); - } catch (Exception e) { - log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e); - } - }); - } - - // ========== 请求队列(per-device 限速) ========== - - /** - * 提交轮询请求到设备请求队列(保证同设备请求间隔) - */ - private void submitPollRequest(Long deviceId, Long pointId) { - // 1. 将请求添加到设备的请求队列 - Queue queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>()); - queue.offer(() -> pollPoint(deviceId, pointId)); - - // 2. 处理设备请求队列(如果没有延迟 timer 在等待) - processDeviceQueue(deviceId); - } - - /** - * 处理设备请求队列 - */ - private void processDeviceQueue(Long deviceId) { - Queue queue = deviceRequestQueues.get(deviceId); - if (CollUtil.isEmpty(queue)) { - return; - } - // 检查是否已有延迟 timer 在等待 - if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) { - return; - } - - // TODO @AI:可以改成不满足间隔,然后 return,简化括号层级; - long now = System.currentTimeMillis(); - long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L); - long elapsed = now - lastTime; - - if (elapsed >= MIN_REQUEST_INTERVAL) { - // 满足间隔要求,立即执行 - Runnable task = queue.poll(); - if (task != null) { - deviceLastRequestTime.put(deviceId, now); - task.run(); - // 继续处理队列中的下一个(如果有的话,需要延迟) - if (!queue.isEmpty()) { - scheduleNextRequest(deviceId); - } - } - } else { - // 需要延迟 - scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed); - } - } - - private void scheduleNextRequest(Long deviceId) { - scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL); - } - - private void scheduleNextRequest(Long deviceId, long delayMs) { - deviceDelayTimerActive.put(deviceId, true); - vertx.setTimer(delayMs, id -> { - deviceDelayTimerActive.put(deviceId, false); - Queue queue = deviceRequestQueues.get(deviceId); - // TODO @AI:if return?简化下? - if (CollUtil.isEmpty(queue)) { - Runnable task = queue.poll(); - if (task != null) { - deviceLastRequestTime.put(deviceId, System.currentTimeMillis()); - task.run(); - } - // 继续处理 - if (queue != null && !queue.isEmpty()) { - scheduleNextRequest(deviceId); - } - } - }); - } - // ========== 轮询执行 ========== /** * 轮询单个点位 - *

                              - * 运行时从 configCacheService 获取最新的 config 和 point,而非使用闭包捕获的旧引用。 */ - private void pollPoint(Long deviceId, Long pointId) { + @Override + protected void pollPoint(Long deviceId, Long pointId) { // 1.1 从 configCache 获取最新配置 IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); if (config == null || CollUtil.isEmpty(config.getPoints())) { @@ -272,34 +63,31 @@ public class IotModbusTcpSlavePollScheduler { return; } // 1.2 查找点位 - IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId)); + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId); if (point == null) { log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId); return; } - // 2. 获取连接信息 - ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(deviceId); - if (connInfo == null) { + // 2.1 获取连接 + ConnectionInfo connection = connectionManager.getConnectionInfoByDeviceId(deviceId); + if (connection == null) { log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId); return; } + // 2.2 获取 slave ID + IotModbusFrameFormatEnum frameFormat = connection.getFrameFormat(); + Assert.notNull(frameFormat, "设备 {} 的帧格式不能为空", deviceId); + int slaveId = connection.getSlaveId(); + Assert.notNull(connection.getSlaveId(), "设备 {} 的 slaveId 不能为空", deviceId); - // 3.1 确定帧格式和事务 ID - IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); - if (frameFormat == null) { - log.warn("[pollPoint][设备 {} 帧格式为空,跳过轮询]", deviceId); - return; - } + // 3.1 编码读请求 Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP ? (transactionIdCounter.incrementAndGet() & 0xFFFF) : null; - // TODO @AI:这里断言必须非空! - int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; - // 3.2 编码读请求 byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(), point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId); - // 3.3 注册 PendingRequest + // 3.2 注册 PendingRequest PendingRequest pendingRequest = new PendingRequest( deviceId, point.getId(), point.getIdentifier(), slaveId, point.getFunctionCode(), @@ -307,41 +95,11 @@ public class IotModbusTcpSlavePollScheduler { transactionId, System.currentTimeMillis() + requestTimeout); pendingRequestManager.addRequest(pendingRequest); - - // 4. 发送读请求 + // 3.3 发送读请求 connectionManager.sendToDevice(deviceId, data); log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]", deviceId, point.getIdentifier(), point.getFunctionCode(), point.getRegisterAddress(), point.getRegisterCount()); } - // ========== 停止 ========== - - /** - * 停止设备的轮询 - */ - public void stopPolling(Long deviceId) { - Map timers = devicePointTimers.remove(deviceId); - if (CollUtil.isEmpty(timers)) { - return; - } - for (PointTimerInfo timerInfo : timers.values()) { - vertx.cancelTimer(timerInfo.getTimerId()); - } - // 清理请求队列 - deviceRequestQueues.remove(deviceId); - deviceLastRequestTime.remove(deviceId); - deviceDelayTimerActive.remove(deviceId); - log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); - } - - /** - * 停止所有轮询 - */ - public void stopAll() { - for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) { - stopPolling(deviceId); - } - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java index c8aa29906a..5f0b547f1a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java +++ b/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; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; import lombok.extern.slf4j.Slf4j; @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber { +public class IotMqttDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { private final IotMqttDownstreamHandler downstreamHandler; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java index 7a29e6c00c..39a73849fb 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java +++ b/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; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; import lombok.extern.slf4j.Slf4j; /** @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { +public class IotTcpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { private final IotTcpDownstreamHandler downstreamHandler; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java index ea0bc99b39..cc21df60e3 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java +++ b/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; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; import lombok.extern.slf4j.Slf4j; /** @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotUdpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { +public class IotUdpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { private final IotUdpDownstreamHandler downstreamHandler; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java index efe5f437e8..c565be2c95 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java +++ b/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 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; import lombok.extern.slf4j.Slf4j; @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotWebSocketDownstreamSubscriber extends IotProtocolDownstreamSubscriber { +public class IotWebSocketDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { private final IotWebSocketDownstreamHandler downstreamHandler; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java index faea89ee9b..e103d6b9d3 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetClient; @@ -285,7 +285,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF); } // 计算 CRC16 - int crc = IotModbusUtils.calculateCrc16(frame, totalLength - 2); + int crc = IotModbusCommonUtils.calculateCrc16(frame, totalLength - 2); frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High return frame; From 4e4c776bed8327d3972ab98ea327d7a8f5b4c72c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 8 Feb 2026 20:48:24 +0800 Subject: [PATCH 39/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9Amodbus-?= =?UTF-8?q?tcp-slave=E3=80=81modbus-tcp-master=20=E7=BB=A7=E7=BB=AD?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=A4=A7=E9=87=8F=E4=BB=A3=E7=A0=81=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E5=AF=B9=E6=8E=A5=E7=AE=A1=E7=90=86=E5=90=8E=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/api/device/IoTDeviceApiImpl.java | 30 +++-- .../modbus/IotDeviceModbusConfigRespVO.java | 8 +- .../IotDeviceModbusConfigSaveReqVO.java | 24 ++-- .../device/IotDeviceModbusConfigDO.java | 2 +- .../device/IotDeviceModbusConfigMapper.java | 9 +- .../device/IotDeviceModbusConfigService.java | 8 +- .../IotDeviceModbusConfigServiceImpl.java | 6 +- .../service/product/IotProductService.java | 21 ++++ .../product/IotProductServiceImpl.java | 5 + .../iot/core/biz/IotDeviceCommonApi.java | 13 +- .../dto/IotModbusDeviceConfigListReqDTO.java | 37 ++++++ .../biz/dto/IotModbusDeviceConfigRespDTO.java | 2 +- .../core/enums/IotModbusFrameFormatEnum.java | 18 ++- .../iot/core/util/IotDeviceAuthUtils.java | 8 ++ .../AbstractIotModbusPollScheduler.java | 9 ++ .../common/utils/IotModbusCommonUtils.java | 15 ++- .../common/utils/IotModbusTcpMasterUtils.java | 18 +-- .../tcpmaster/IotModbusTcpMasterProtocol.java | 77 ++++-------- ... IotModbusTcpMasterDownstreamHandler.java} | 21 ++-- ...tModbusTcpMasterDownstreamSubscriber.java} | 10 +- ...=> IotModbusTcpMasterUpstreamHandler.java} | 9 +- ...IotModbusTcpMasterConfigCacheService.java} | 32 +++-- ... IotModbusTcpMasterConnectionManager.java} | 117 ++++++++++++++---- ...a => IotModbusTcpMasterPollScheduler.java} | 20 +-- .../modbus/tcpmaster/package-info.java | 7 +- .../tcpslave/IotModbusTcpSlaveProtocol.java | 40 +++--- .../modbus/tcpslave/codec/IotModbusFrame.java | 5 +- .../tcpslave/codec/IotModbusFrameDecoder.java | 17 ++- .../IotModbusTcpSlaveDownstreamHandler.java | 23 ++-- .../IotModbusTcpSlaveUpstreamHandler.java | 45 ++++--- .../IotModbusTcpSlaveConfigCacheService.java | 107 +++++++--------- .../IotModbusTcpSlaveConnectionManager.java | 15 ++- ...otModbusTcpSlavePendingRequestManager.java | 38 +++++- .../IotModbusTcpSlavePollScheduler.java | 14 ++- .../modbus/tcpslave/package-info.java | 6 + .../device/remote/IotDeviceApiImpl.java | 5 +- .../src/main/resources/application.yaml | 2 +- ...odbusTcpSlaveModbusTcpIntegrationTest.java | 8 +- 38 files changed, 522 insertions(+), 329 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigListReqDTO.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/{IotModbusTcpDownstreamHandler.java => IotModbusTcpMasterDownstreamHandler.java} (83%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/{IotModbusTcpDownstreamSubscriber.java => IotModbusTcpMasterDownstreamSubscriber.java} (64%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/{IotModbusTcpUpstreamHandler.java => IotModbusTcpMasterUpstreamHandler.java} (89%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/{IotModbusTcpConfigCacheService.java => IotModbusTcpMasterConfigCacheService.java} (69%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/{IotModbusTcpConnectionManager.java => IotModbusTcpMasterConnectionManager.java} (62%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/{IotModbusTcpPollScheduler.java => IotModbusTcpMasterPollScheduler.java} (75%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/package-info.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java index 048b79de80..6bfe0e4e92 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java @@ -1,16 +1,14 @@ package cn.iocoder.yudao.module.iot.api.device; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.RpcConstants; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.*; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; @@ -35,8 +33,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.List; - import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; @@ -81,12 +77,13 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { } @Override - @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/enabled-configs") + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/config-list") @PermitAll @TenantIgnore - public CommonResult> getEnabledModbusDeviceConfigs() { - // 1. 获取所有启用的 Modbus 连接配置 - List configList = modbusConfigService.getEnabledDeviceModbusConfigList(); + public CommonResult> getModbusDeviceConfigList( + @RequestBody IotModbusDeviceConfigListReqDTO listReqDTO) { + // 1. 获取 Modbus 连接配置 + List configList = modbusConfigService.getDeviceModbusConfigList(listReqDTO); if (CollUtil.isEmpty(configList)) { return success(new ArrayList<>()); } @@ -95,6 +92,7 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { Set deviceIds = convertSet(configList, IotDeviceModbusConfigDO::getDeviceId); Map deviceMap = deviceService.getDeviceMap(deviceIds); Map> pointMap = modbusPointService.getEnabledDeviceModbusPointMapByDeviceIds(deviceIds); + Map productMap = productService.getProductMap(convertSet(deviceMap.values(), IotDeviceDO::getProductId)); List result = new ArrayList<>(configList.size()); for (IotDeviceModbusConfigDO config : configList) { // 3.1 获取设备信息 @@ -102,12 +100,20 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { if (device == null) { continue; } - // 3.2 获取启用的点位列表 + // 3.2 按 protocolType 筛选(如果非空) + if (StrUtil.isNotEmpty(listReqDTO.getProtocolType())) { + IotProductDO product = productMap.get(device.getProductId()); + if (product == null || ObjUtil.notEqual(listReqDTO.getProtocolType(), product.getProtocolType())) { + continue; + } + } + // 3.3 获取启用的点位列表 List pointList = pointMap.get(config.getDeviceId()); if (CollUtil.isEmpty(pointList)) { continue; } - // 3.3 构建 IotModbusDeviceConfigRespDTO 对象 + + // 3.4 构建 IotModbusDeviceConfigRespDTO 对象 IotModbusDeviceConfigRespDTO configDTO = BeanUtils.toBean(config, IotModbusDeviceConfigRespDTO.class, o -> o.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()) .setPoints(BeanUtils.toBean(pointList, IotModbusPointRespDTO.class))); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java index 60b132c3d1..ecce04de6e 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java @@ -33,13 +33,11 @@ public class IotDeviceModbusConfigRespVO { @Schema(description = "重试间隔(毫秒)", example = "1000") private Integer retryInterval; - // TODO @AI:不要【:1-云端轮询 2-主动上报】 - @Schema(description = "模式:1-云端轮询 2-主动上报", example = "1") + @Schema(description = "工作模式", example = "1") private Integer mode; - // TODO @AI:还是换成 int,然后写注释;不要【:modbus_tcp / modbus_rtu】 - @Schema(description = "数据帧格式:modbus_tcp / modbus_rtu", example = "modbus_tcp") - private String frameFormat; + @Schema(description = "数据帧格式", example = "1") + private Integer frameFormat; @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") private Integer status; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java index ae47afcd89..9fa3fdc7c2 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -13,12 +15,14 @@ public class IotDeviceModbusConfigSaveReqVO { @NotNull(message = "设备编号不能为空") private Long deviceId; - @Schema(description = "Modbus 服务器 IP 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100") - @NotEmpty(message = "Modbus 服务器 IP 地址不能为空") + @Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100") +// @NotEmpty(message = "Modbus 服务器 IP 地址不能为空") + // TODO @AI:这个字段,要根据情况校验; private String ip; - @Schema(description = "Modbus 端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "502") - @NotNull(message = "Modbus 端口不能为空") + @Schema(description = "Modbus 端口", example = "502") +// @NotNull(message = "Modbus 端口不能为空") + // TODO @AI:这个字段,要根据情况校验; private Integer port; @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @@ -31,13 +35,13 @@ public class IotDeviceModbusConfigSaveReqVO { @Schema(description = "重试间隔(毫秒)", example = "1000") private Integer retryInterval; - // TODO @AI:不要【:1-云端轮询 2-主动上报】 - @Schema(description = "模式:1-云端轮询 2-主动上报", example = "1") + @Schema(description = "工作模式", example = "1") + @InEnum(IotModbusModeEnum.class) private Integer mode; - // TODO @AI:不要【:1-云端轮询 2-主动上报】 - @Schema(description = "数据帧格式:modbus_tcp / modbus_rtu", example = "modbus_tcp") - private String frameFormat; + @Schema(description = "数据帧格式", example = "1") + @InEnum(IotModbusFrameFormatEnum.class) + private Integer frameFormat; @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") @NotNull(message = "状态不能为空") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java index 06e94d2506..0204908d9e 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java @@ -69,7 +69,7 @@ public class IotDeviceModbusConfigDO extends TenantBaseDO { * * @see cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum */ - private String frameFormat; + private Integer frameFormat; /** * 状态 * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java index 397a3884f4..b18769c6a6 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.iot.dal.mysql.device; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; import org.apache.ibatis.annotations.Mapper; @@ -18,8 +20,11 @@ public interface IotDeviceModbusConfigMapper extends BaseMapperX selectListByStatus(Integer status) { - return selectList(IotDeviceModbusConfigDO::getStatus, status); + default List selectList(IotModbusDeviceConfigListReqDTO reqDTO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(IotDeviceModbusConfigDO::getStatus, reqDTO.getStatus()) + .eqIfPresent(IotDeviceModbusConfigDO::getMode, reqDTO.getMode()) + .inIfPresent(IotDeviceModbusConfigDO::getDeviceId, reqDTO.getDeviceIds())); } } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java index 1bd17e4183..2d9ef7ec61 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.iot.service.device; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; import jakarta.validation.Valid; @@ -37,10 +38,11 @@ public interface IotDeviceModbusConfigService { IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId); /** - * 获得所有启用的 Modbus 连接配置列表 + * 获得 Modbus 连接配置列表 * - * @return 启用的 Modbus 连接配置列表 + * @param listReqDTO 查询参数 + * @return Modbus 连接配置列表 */ - List getEnabledDeviceModbusConfigList(); + List getDeviceModbusConfigList(IotModbusDeviceConfigListReqDTO listReqDTO); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java index 91388d2cf6..b92e9948e3 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.iot.service.device; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusConfigMapper; import jakarta.annotation.Resource; @@ -54,8 +54,8 @@ public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigSe } @Override - public List getEnabledDeviceModbusConfigList() { - return modbusConfigMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + public List getDeviceModbusConfigList(IotModbusDeviceConfigListReqDTO listReqDTO) { + return modbusConfigMapper.selectList(listReqDTO); } } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java index d4292ef521..3d68fb59e1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java +++ b/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; import java.time.LocalDateTime; import java.util.Collection; import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; /** * IoT 产品 Service 接口 @@ -121,6 +124,24 @@ public interface IotProductService { */ Long getProductCount(@Nullable LocalDateTime createTime); + /** + * 批量获得产品列表 + * + * @param ids 产品编号集合 + * @return 产品列表 + */ + List getProductList(Collection ids); + + /** + * 批量获得产品 Map + * + * @param ids 产品编号集合 + * @return 产品 Map(key: 产品编号, value: 产品) + */ + default Map getProductMap(Collection ids) { + return convertMap(getProductList(ids), IotProductDO::getId); + } + /** * 批量校验产品存在 * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java index e001f46a2b..9686f42c93 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java @@ -171,6 +171,11 @@ public class IotProductServiceImpl implements IotProductService { return productMapper.selectCountByCreateTime(createTime); } + @Override + public List getProductList(Collection ids) { + return productMapper.selectByIds(ids); + } + @Override public void validateProductsExist(Collection ids) { if (CollUtil.isEmpty(ids)) { diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java index 54a0e67a41..c0b3f9df31 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java @@ -1,13 +1,7 @@ package cn.iocoder.yudao.module.iot.core.biz; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; - -import java.util.List; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.*; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; @@ -54,10 +48,11 @@ public interface IotDeviceCommonApi { CommonResult> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO); /** - * 获取所有启用的 Modbus 设备配置列表 + * 获取 Modbus 设备配置列表 * + * @param listReqDTO 查询参数 * @return Modbus 设备配置列表 */ - CommonResult> getEnabledModbusDeviceConfigs(); + CommonResult> getModbusDeviceConfigList(IotModbusDeviceConfigListReqDTO listReqDTO); } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigListReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigListReqDTO.java new file mode 100644 index 0000000000..7865a09f00 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigListReqDTO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.core.biz.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.Set; + +/** + * IoT Modbus 设备配置列表查询 Request DTO + * + * @author 芋道源码 + */ +@Data +@Accessors(chain = true) +public class IotModbusDeviceConfigListReqDTO { + + /** + * 状态 + */ + private Integer status; + + /** + * 模式 + */ + private Integer mode; + + /** + * 协议类型 + */ + private String protocolType; + + /** + * 设备 ID 集合 + */ + private Set deviceIds; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java index 4580a8e596..683bcef4c4 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java @@ -54,7 +54,7 @@ public class IotModbusDeviceConfigRespDTO { /** * 数据帧格式 */ - private String frameFormat; + private Integer frameFormat; // ========== Modbus 点位配置 ========== diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java index b2817047a1..4e963850eb 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java @@ -13,26 +13,22 @@ import java.util.Arrays; */ @Getter @RequiredArgsConstructor -public enum IotModbusFrameFormatEnum implements ArrayValuable { +public enum IotModbusFrameFormatEnum implements ArrayValuable { - MODBUS_TCP("modbus_tcp", "Modbus TCP"), - MODBUS_RTU("modbus_rtu", "Modbus RTU"); + MODBUS_TCP(1), + MODBUS_RTU(2); - public static final String[] ARRAYS = Arrays.stream(values()) + public static final Integer[] ARRAYS = Arrays.stream(values()) .map(IotModbusFrameFormatEnum::getFormat) - .toArray(String[]::new); + .toArray(Integer[]::new); /** * 格式 */ - private final String format; - /** - * 名称 - */ - private final String name; + private final Integer format; @Override - public String[] array() { + public Integer[] array() { return ARRAYS; } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java index 609d0a60ae..1aa9cfcabf 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java +++ b/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 { return String.format("%s.%s", productKey, deviceName); } + public static String buildClientIdFromUsername(String username) { + IotDeviceIdentity identity = parseUsername(username); + if (identity == null) { + return null; + } + return buildClientId(identity.getProductKey(), identity.getDeviceName()); + } + public static String buildUsername(String productKey, String deviceName) { return String.format("%s&%s", deviceName, productKey); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java index dedac7acd3..e62f85fcf6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java @@ -32,6 +32,10 @@ public abstract class AbstractIotModbusPollScheduler { * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积 */ private static final long MIN_REQUEST_INTERVAL = 1000; + /** + * 每个设备请求队列的最大长度,超出时丢弃最旧请求 + */ + private static final int MAX_QUEUE_SIZE = 1000; /** * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) @@ -159,6 +163,11 @@ public abstract class AbstractIotModbusPollScheduler { private void submitPollRequest(Long deviceId, Long pointId) { // 1. 【重要】将请求添加到设备的请求队列 Queue queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>()); + while (queue.size() >= MAX_QUEUE_SIZE) { + // 超出上限时,丢弃最旧的请求 + queue.poll(); + log.warn("[submitPollRequest][设备 {} 请求队列已满({}), 丢弃最旧请求]", deviceId, MAX_QUEUE_SIZE); + } queue.offer(() -> pollPoint(deviceId, pointId)); // 2. 处理设备请求队列(如果没有延迟 timer 在等待) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java index 236277bc0a..23ee4bf124 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; @@ -331,10 +332,10 @@ public class IotModbusCommonUtils { // 其他字节序调整 byte[] result = new byte[bytes.length]; switch (byteOrderEnum) { - case BA: // 小端序(16 位) - if (bytes.length >= 2) { - result[0] = bytes[1]; - result[1] = bytes[0]; + case BA: // 小端序:按每 2 字节一组交换(16 位场景 [1,0],32 位场景 [1,0,3,2]) + for (int i = 0; i + 1 < bytes.length; i += 2) { + result[i] = bytes[i + 1]; + result[i + 1] = bytes[i]; } break; case CDAB: // 大端字交换(32 位) @@ -509,6 +510,9 @@ public class IotModbusCommonUtils { * @return 匹配的点位配置,未找到返回 null */ public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { + if (config == null || StrUtil.isBlank(identifier)) { + return null; + } return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); } @@ -520,6 +524,9 @@ public class IotModbusCommonUtils { * @return 匹配的点位配置,未找到返回 null */ public static IotModbusPointRespDTO findPointById(IotModbusDeviceConfigRespDTO config, Long pointId) { + if (config == null || pointId == null) { + return null; + } return CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId)); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java index 5804ea6022..1f7c14dc41 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConnectionManager; import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; import com.ghgande.j2mod.modbus.msg.*; import com.ghgande.j2mod.modbus.procimg.InputRegister; @@ -19,7 +19,7 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.I *

                              * 封装基于 j2mod 的 Modbus TCP 读写操作: * 1. 根据功能码创建对应的 Modbus 读/写请求 - * 2. 通过 {@link IotModbusTcpConnectionManager.ModbusConnection} 执行事务 + * 2. 通过 {@link IotModbusTcpMasterConnectionManager.ModbusConnection} 执行事务 * 3. 从响应中提取原始值 * * @author 芋道源码 @@ -36,9 +36,9 @@ public class IotModbusTcpMasterUtils { * @param point 点位配置 * @return 原始值(int 数组) */ - public static Future read(IotModbusTcpConnectionManager.ModbusConnection connection, - Integer slaveId, - IotModbusPointRespDTO point) { + public static Future read(IotModbusTcpMasterConnectionManager.ModbusConnection connection, + Integer slaveId, + IotModbusPointRespDTO point) { return connection.executeBlocking(tcpConnection -> { try { // 1. 创建请求 @@ -70,10 +70,10 @@ public class IotModbusTcpMasterUtils { * @param values 要写入的值 * @return 是否成功 */ - public static Future write(IotModbusTcpConnectionManager.ModbusConnection connection, - Integer slaveId, - IotModbusPointRespDTO point, - int[] values) { + public static Future write(IotModbusTcpMasterConnectionManager.ModbusConnection connection, + Integer slaveId, + IotModbusPointRespDTO point, + int[] values) { return connection.executeBlocking(tcpConnection -> { try { // 1. 创建请求 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java index 7b435d74fa..1fac973a92 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java @@ -6,16 +6,15 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpMasterDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpMasterDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpMasterUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterPollScheduler; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; import lombok.Getter; @@ -23,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import org.redisson.api.RedissonClient; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -61,15 +61,14 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { /** * 连接管理器 */ - private final IotModbusTcpConnectionManager connectionManager; + private final IotModbusTcpMasterConnectionManager connectionManager; /** * 下行消息订阅者 */ - private final IotModbusTcpDownstreamSubscriber downstreamSubscriber; + private final IotModbusTcpMasterDownstreamSubscriber downstreamSubscriber; - private final IotModbusTcpConfigCacheService configCacheService; - private final IotModbusTcpPollScheduler pollScheduler; - private final IotDeviceMessageService messageService; + private final IotModbusTcpMasterConfigCacheService configCacheService; + private final IotModbusTcpMasterPollScheduler pollScheduler; public IotModbusTcpMasterProtocol(ProtocolProperties properties) { IotModbusTcpMasterConfig modbusTcpMasterConfig = properties.getModbusTcpMaster(); @@ -83,22 +82,23 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { // 初始化 Manager RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class); IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.connectionManager = new IotModbusTcpConnectionManager(redissonClient, vertx); - this.configCacheService = new IotModbusTcpConfigCacheService(deviceApi); + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.configCacheService = new IotModbusTcpMasterConfigCacheService(deviceApi); + // DONE @AI:上线/下线消息已移到 ConnectionManager 内部处理,不再走回调 + this.connectionManager = new IotModbusTcpMasterConnectionManager(redissonClient, vertx, + messageService, configCacheService, serverId); // 初始化 Handler - this.messageService = SpringUtil.getBean(IotDeviceMessageService.class); - IotDeviceMessageService messageService = this.messageService; - IotModbusTcpUpstreamHandler upstreamHandler = new IotModbusTcpUpstreamHandler(messageService, serverId); - IotModbusTcpDownstreamHandler downstreamHandler = new IotModbusTcpDownstreamHandler(connectionManager, + IotModbusTcpMasterUpstreamHandler upstreamHandler = new IotModbusTcpMasterUpstreamHandler(messageService, serverId); + IotModbusTcpMasterDownstreamHandler downstreamHandler = new IotModbusTcpMasterDownstreamHandler(connectionManager, configCacheService); // 初始化轮询调度器 - this.pollScheduler = new IotModbusTcpPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService); + this.pollScheduler = new IotModbusTcpMasterPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService); // 初始化下行消息订阅者 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - this.downstreamSubscriber = new IotModbusTcpDownstreamSubscriber(this, downstreamHandler, messageBus); + this.downstreamSubscriber = new IotModbusTcpMasterDownstreamSubscriber(this, downstreamHandler, messageBus); } @Override @@ -196,46 +196,21 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { // 2. 更新连接和轮询任务 for (IotModbusDeviceConfigRespDTO config : configs) { try { - // 2.1 检测是否为首次连接 - boolean isNewConnection = connectionManager.getConnection(config.getDeviceId()) == null; - // 2.2 确保连接存在 + // 2.1 确保连接存在 connectionManager.ensureConnection(config); - // 2.3 首次建连成功后发送上线消息 - // TODO @AI:在这里判断上线 ,会不会有点奇怪??? - if (isNewConnection && connectionManager.getConnection(config.getDeviceId()) != null) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - messageService.sendDeviceMessage(onlineMessage, - config.getProductKey(), config.getDeviceName(), serverId); - } catch (Exception ex) { - log.error("[refreshConfig][发送设备上线消息失败, deviceId={}]", config.getDeviceId(), ex); - } - } - // 2.4 更新轮询任务 + // 2.2 更新轮询任务 pollScheduler.updatePolling(config); } catch (Exception e) { log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); } } - // 3. 清理已删除设备的资源(仅 API 成功时才执行) - configCacheService.cleanupRemovedDevices(configs, deviceId -> { - // 3.1 发送设备下线消息 - // TODO @AI:在这里判断上线 ,会不会有点奇怪??? - IotModbusDeviceConfigRespDTO removedConfig = configCacheService.getConfig(deviceId); - if (removedConfig != null) { - try { - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - messageService.sendDeviceMessage(offlineMessage, - removedConfig.getProductKey(), removedConfig.getDeviceName(), serverId); - } catch (Exception ex) { - log.error("[refreshConfig][发送设备下线消息失败, deviceId={}]", deviceId, ex); - } - } - // 3.2 停止轮询和移除连接 + // 3. 清理已删除设备的资源 + Set removedDeviceIds = configCacheService.cleanupRemovedDevices(configs); + for (Long deviceId : removedDeviceIds) { pollScheduler.stopPolling(deviceId); connectionManager.removeDevice(deviceId); - }); + } } catch (Exception e) { log.error("[refreshConfig][刷新配置失败]", e); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamHandler.java similarity index 83% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamHandler.java index 32dfde5a64..af51e22933 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamHandler.java @@ -1,41 +1,42 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpMasterUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConnectionManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import java.util.Map; /** - * IoT Modbus TCP 下行消息处理器 + * IoT Modbus TCP Master 下行消息处理器 *

                              * 负责: * 1. 处理下行消息(如属性设置 thing.service.property.set) - * 2. 执行 Modbus 写入操作 + * 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备 * * @author 芋道源码 */ @RequiredArgsConstructor @Slf4j -public class IotModbusTcpDownstreamHandler { +public class IotModbusTcpMasterDownstreamHandler { - private final IotModbusTcpConnectionManager connectionManager; - private final IotModbusTcpConfigCacheService configCacheService; + private final IotModbusTcpMasterConnectionManager connectionManager; + private final IotModbusTcpMasterConfigCacheService configCacheService; /** * 处理下行消息 */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "DuplicatedCode"}) public void handle(IotDeviceMessage message) { // 1.1 检查是否是属性设置消息 - if (!IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod().equals(message.getMethod())) { + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); return; } @@ -78,7 +79,7 @@ public class IotModbusTcpDownstreamHandler { */ private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) { // 1.1 获取连接 - IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); + IotModbusTcpMasterConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); if (connection == null) { log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId()); return; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamSubscriber.java similarity index 64% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamSubscriber.java index 74afcbb38f..812e13da01 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamSubscriber.java @@ -12,13 +12,13 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotModbusTcpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { +public class IotModbusTcpMasterDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { - private final IotModbusTcpDownstreamHandler downstreamHandler; + private final IotModbusTcpMasterDownstreamHandler downstreamHandler; - public IotModbusTcpDownstreamSubscriber(IotModbusTcpMasterProtocol protocol, - IotModbusTcpDownstreamHandler downstreamHandler, - IotMessageBus messageBus) { + public IotModbusTcpMasterDownstreamSubscriber(IotModbusTcpMasterProtocol protocol, + IotModbusTcpMasterDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { super(protocol, messageBus); this.downstreamHandler = downstreamHandler; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpMasterUpstreamHandler.java similarity index 89% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpMasterUpstreamHandler.java index de02af06c2..7fc573d950 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpMasterUpstreamHandler.java @@ -17,13 +17,14 @@ import java.util.Map; * @author 芋道源码 */ @Slf4j -public class IotModbusTcpUpstreamHandler { +public class IotModbusTcpMasterUpstreamHandler { private final IotDeviceMessageService messageService; + private final String serverId; - public IotModbusTcpUpstreamHandler(IotDeviceMessageService messageService, - String serverId) { + public IotModbusTcpMasterUpstreamHandler(IotDeviceMessageService messageService, + String serverId) { this.messageService = messageService; this.serverId = serverId; } @@ -39,7 +40,7 @@ public class IotModbusTcpUpstreamHandler { IotModbusPointRespDTO point, int[] rawValue) { try { - // 1.1 转换原始值为物模型属性值 + // 1.1 转换原始值为物模型属性值(点位翻译) Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValue, point); log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]", config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java similarity index 69% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConfigCacheService.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java index 5a3386e1d2..7ce6dd02fc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java @@ -1,25 +1,31 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.*; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; /** - * IoT Modbus TCP 配置缓存服务,负责:从 biz 拉取 Modbus 设备配置,缓存配置数据,并检测配置变更 + * IoT Modbus TCP Master 配置缓存服务 * * @author 芋道源码 */ @RequiredArgsConstructor @Slf4j -public class IotModbusTcpConfigCacheService { +public class IotModbusTcpMasterConfigCacheService { private final IotDeviceCommonApi deviceApi; @@ -31,7 +37,7 @@ public class IotModbusTcpConfigCacheService { /** * 已知的设备 ID 集合(作用:用于检测已删除的设备) * - * @see #cleanupRemovedDevices(List, Consumer) + * @see #cleanupRemovedDevices(List) */ private final Set knownDeviceIds = ConcurrentHashMap.newKeySet(); @@ -43,7 +49,9 @@ public class IotModbusTcpConfigCacheService { public List refreshConfig() { try { // 1. 从远程获取配置 - CommonResult> result = deviceApi.getEnabledModbusDeviceConfigs(); + CommonResult> result = deviceApi.getModbusDeviceConfigList( + new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setMode(IotModbusModeEnum.POLLING.getMode()).setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_MASTER.getType())); result.checkError(); List configs = result.getData(); @@ -69,28 +77,30 @@ public class IotModbusTcpConfigCacheService { } /** - * 清理已删除设备的资源,并更新已知设备 ID 集合 + * 计算已删除设备的 ID 集合,清理缓存,并更新已知设备 ID 集合 + * + * DONE @AI:不再使用 callback 模式,返回已删除的设备 ID 集合,由调用方直接清理 * * @param currentConfigs 当前有效的配置列表 - * @param cleanupAction 清理动作 + * @return 已删除的设备 ID 集合 */ - public void cleanupRemovedDevices(List currentConfigs, Consumer cleanupAction) { + public Set cleanupRemovedDevices(List currentConfigs) { // 1.1 获取当前有效的设备 ID Set currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId); // 1.2 找出已删除的设备(基于旧的 knownDeviceIds) Set removedDeviceIds = new HashSet<>(knownDeviceIds); removedDeviceIds.removeAll(currentDeviceIds); - // 2. 清理已删除设备(先执行 cleanupAction,再从缓存移除,保证 action 中仍可获取 config) + // 2. 清理已删除设备的缓存 for (Long deviceId : removedDeviceIds) { log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); - cleanupAction.accept(deviceId); configCache.remove(deviceId); } // 3. 更新已知设备 ID 集合为当前有效的设备 ID knownDeviceIds.clear(); knownDeviceIds.addAll(currentDeviceIds); + return removedDeviceIds; } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java similarity index 62% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConnectionManager.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java index 3b4e104402..34b68f65b6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java @@ -2,12 +2,13 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager; import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import com.ghgande.j2mod.modbus.net.TCPMasterConnection; import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Vertx; import lombok.Data; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; @@ -26,14 +27,16 @@ import java.util.concurrent.ConcurrentHashMap; * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j -public class IotModbusTcpConnectionManager { +public class IotModbusTcpMasterConnectionManager { private static final String LOCK_KEY_PREFIX = "iot:modbus-tcp:connection:"; private final RedissonClient redissonClient; private final Vertx vertx; + private final IotDeviceMessageService messageService; + private final IotModbusTcpMasterConfigCacheService configCacheService; + private final String serverId; /** * 连接池:key = ip:port @@ -45,8 +48,21 @@ public class IotModbusTcpConnectionManager { */ private final Map deviceConnectionMap = new ConcurrentHashMap<>(); + public IotModbusTcpMasterConnectionManager(RedissonClient redissonClient, Vertx vertx, + IotDeviceMessageService messageService, + IotModbusTcpMasterConfigCacheService configCacheService, + String serverId) { + this.redissonClient = redissonClient; + this.vertx = vertx; + this.messageService = messageService; + this.configCacheService = configCacheService; + this.serverId = serverId; + } + /** * 确保连接存在 + *

                              + * 首次建连成功时,直接发送设备上线消息 * * @param config 设备配置 */ @@ -59,37 +75,39 @@ public class IotModbusTcpConnectionManager { config.getDeviceId(), oldConnectionKey, connectionKey); removeDevice(config.getDeviceId()); } - // 1.2 记录设备与连接的关系 + // 1.2 记录设备与连接的映射 deviceConnectionMap.put(config.getDeviceId(), connectionKey); - // 2. 情况一:连接已存在,添加设备引用 + // 2. 情况一:连接已存在,注册设备并发送上线消息 ModbusConnection connection = connectionPool.get(connectionKey); if (connection != null) { - connection.addDevice(config.getDeviceId(), config.getSlaveId()); + addDeviceAndOnline(connection, config); return; } - // 3. 情况二:连接不存在,创建新连接 - // 3.1 尝试获取分布式锁 + // 3. 情况二:连接不存在,加分布式锁创建新连接 RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + connectionKey); if (!lock.tryLock()) { log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey); return; } - // 3.2 double-check:拿到锁后再次检查,避免并发创建重复连接 try { + // 3.1 double-check:拿到锁后再次检查,避免并发创建重复连接 connection = connectionPool.get(connectionKey); if (connection != null) { - connection.addDevice(config.getDeviceId(), config.getSlaveId()); - lock.unlock(); + addDeviceAndOnline(connection, config); return; } - // 3.3 创建新连接 - connection = createConnection(config, lock); + // 3.2 创建新连接 + connection = createConnection(config); connectionPool.put(connectionKey, connection); log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey); + // 3.3 注册设备并发送上线消息 + addDeviceAndOnline(connection, config); } catch (Exception e) { log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e); + } finally { + // TODO @AI:如果这里释放,会不会出现,集群模式下,多个节点同时创建连接的情况?需要验证一下 Redisson 的分布式锁特性?! lock.unlock(); } } @@ -97,7 +115,7 @@ public class IotModbusTcpConnectionManager { /** * 创建 Modbus TCP 连接 */ - private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config, RLock lock) throws Exception { + private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config) throws Exception { // 1. 创建 TCP 连接 TCPMasterConnection tcpConnection = new TCPMasterConnection(InetAddress.getByName(config.getIp())); tcpConnection.setPort(config.getPort()); @@ -105,12 +123,10 @@ public class IotModbusTcpConnectionManager { tcpConnection.connect(); // 2. 创建 Modbus 连接对象 - ModbusConnection connection = new ModbusConnection() + return new ModbusConnection() .setConnectionKey(buildConnectionKey(config.getIp(), config.getPort())) - .setTcpConnection(tcpConnection).setLock(lock).setContext(vertx.getOrCreateContext()) + .setTcpConnection(tcpConnection).setContext(vertx.getOrCreateContext()) .setTimeout(config.getTimeout()).setRetryInterval(config.getRetryInterval()); - connection.addDevice(config.getDeviceId(), config.getSlaveId()); - return connection; } /** @@ -137,25 +153,71 @@ public class IotModbusTcpConnectionManager { /** * 移除设备 + *

                              + * 移除时直接发送设备下线消息 */ public void removeDevice(Long deviceId) { - // 1. 移除设备引用 + // 1.1 移除设备时,发送下线消息 + sendOfflineMessage(deviceId); + // 1.2 移除设备引用 String connectionKey = deviceConnectionMap.remove(deviceId); if (connectionKey == null) { return; } + + // 2.1 移除连接中的设备引用 ModbusConnection connection = connectionPool.get(connectionKey); if (connection == null) { return; } connection.removeDevice(deviceId); - - // 2. 如果没有设备引用了,关闭连接 + // 2.2 如果没有设备引用了,关闭连接 if (connection.getDeviceCount() == 0) { closeConnection(connectionKey); } } + // ==================== 设备连接 & 上下线消息 ==================== + + /** + * 注册设备到连接,并发送上线消息 + */ + private void addDeviceAndOnline(ModbusConnection connection, + IotModbusDeviceConfigRespDTO config) { + connection.addDevice(config.getDeviceId(), config.getSlaveId()); + sendOnlineMessage(config); + } + + /** + * 发送设备上线消息 + */ + private void sendOnlineMessage(IotModbusDeviceConfigRespDTO config) { + try { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + messageService.sendDeviceMessage(onlineMessage, + config.getProductKey(), config.getDeviceName(), serverId); + } catch (Exception ex) { + log.error("[sendOnlineMessage][发送设备上线消息失败, deviceId={}]", config.getDeviceId(), ex); + } + } + + /** + * 发送设备下线消息 + */ + private void sendOfflineMessage(Long deviceId) { + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); + if (config == null) { + return; + } + try { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + messageService.sendDeviceMessage(offlineMessage, + config.getProductKey(), config.getDeviceName(), serverId); + } catch (Exception ex) { + log.error("[sendOfflineMessage][发送设备下线消息失败, deviceId={}]", deviceId, ex); + } + } + /** * 关闭指定连接 */ @@ -170,10 +232,10 @@ public class IotModbusTcpConnectionManager { connection.getTcpConnection().close(); } // 强制解锁,避免死锁(正常情况下应该不会发生锁未释放的情况) - RLock lock = connection.getLock(); - if (lock != null && lock.isLocked()) { - lock.forceUnlock(); - } +// RLock lock = connection.getLock(); +// if (lock != null && lock.isLocked()) { +// lock.forceUnlock(); +// } log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey); } catch (Exception e) { log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e); @@ -202,11 +264,14 @@ public class IotModbusTcpConnectionManager { private String connectionKey; private TCPMasterConnection tcpConnection; - private RLock lock; private Integer timeout; private Integer retryInterval; + private Context context; + // TODO @AI:是不是需要 lock?!避免集群模式下的竞争(肯定不能让别的节点连接上)!!!【另外,RLock 在节点(持有所锁的节点) cransh 的时候,会自动释放】 +// private RLock lock; + /** * 设备 ID 到 slave ID 的映射 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterPollScheduler.java similarity index 75% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterPollScheduler.java index 2f049561c9..df87b91fa1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterPollScheduler.java @@ -7,7 +7,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpMasterUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpMasterUpstreamHandler; import io.vertx.core.Vertx; import lombok.extern.slf4j.Slf4j; @@ -17,16 +17,16 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotModbusTcpPollScheduler extends AbstractIotModbusPollScheduler { +public class IotModbusTcpMasterPollScheduler extends AbstractIotModbusPollScheduler { - private final IotModbusTcpConnectionManager connectionManager; - private final IotModbusTcpUpstreamHandler upstreamHandler; - private final IotModbusTcpConfigCacheService configCacheService; + private final IotModbusTcpMasterConnectionManager connectionManager; + private final IotModbusTcpMasterUpstreamHandler upstreamHandler; + private final IotModbusTcpMasterConfigCacheService configCacheService; - public IotModbusTcpPollScheduler(Vertx vertx, - IotModbusTcpConnectionManager connectionManager, - IotModbusTcpUpstreamHandler upstreamHandler, - IotModbusTcpConfigCacheService configCacheService) { + public IotModbusTcpMasterPollScheduler(Vertx vertx, + IotModbusTcpMasterConnectionManager connectionManager, + IotModbusTcpMasterUpstreamHandler upstreamHandler, + IotModbusTcpMasterConfigCacheService configCacheService) { super(vertx); this.connectionManager = connectionManager; this.upstreamHandler = upstreamHandler; @@ -54,7 +54,7 @@ public class IotModbusTcpPollScheduler extends AbstractIotModbusPollScheduler { } // 2.1 获取连接 - IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId); + IotModbusTcpMasterConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId); if (connection == null) { log.warn("[pollPoint][设备 {} 没有连接]", deviceId); return; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/package-info.java index f971d3f862..86e393233a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/package-info.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/package-info.java @@ -1,9 +1,6 @@ /** - * Modbus TCP Master 协议实现包 + * Modbus TCP Master(主站)协议:网关主动连接并轮询 Modbus 从站设备 *

                              - * 提供基于 j2mod 的 Modbus TCP 主站(Master)功能,支持: - * 1. 定时轮询 Modbus 从站设备数据 - * 2. 下发属性设置命令到从站设备 - * 3. 数据格式转换(寄存器值 ↔ 物模型属性值) + * 基于 j2mod 实现,支持 FC01-04 读、FC05/06/15/16 写,定时轮询 + 下发属性设置 */ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java index b042363acc..3a44a189fe 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java @@ -81,22 +81,27 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { */ private Long requestCleanupTimerId; - // ========== 各组件 ========== - // TODO @芋艿:稍后排序下,有点小乱; - - private final IotModbusTcpSlaveConfig slaveConfig; - private final IotModbusFrameDecoder frameDecoder; - private final IotModbusFrameEncoder frameEncoder; + /** + * 连接管理器 + */ private final IotModbusTcpSlaveConnectionManager connectionManager; + /** + * 下行消息订阅者 + */ + private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; + + private final IotModbusFrameDecoder frameDecoder; + @SuppressWarnings("FieldCanBeLocal") + private final IotModbusFrameEncoder frameEncoder; + private final IotModbusTcpSlaveConfigCacheService configCacheService; private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; private final IotModbusTcpSlaveUpstreamHandler upstreamHandler; - private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; private final IotModbusTcpSlavePollScheduler pollScheduler; private final IotDeviceMessageService messageService; public IotModbusTcpSlaveProtocol(ProtocolProperties properties) { - this.slaveConfig = properties.getModbusTcpSlave(); + IotModbusTcpSlaveConfig slaveConfig = properties.getModbusTcpSlave(); Assert.notNull(slaveConfig, "Modbus TCP Slave 协议配置(modbusTcpSlave)不能为空"); this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); @@ -124,10 +129,9 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { // 初始化 Handler this.messageService = SpringUtil.getBean(IotDeviceMessageService.class); - IotDeviceMessageService messageService = this.messageService; IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler( - deviceApi, messageService, frameEncoder, + deviceApi, this.messageService, frameEncoder, connectionManager, configCacheService, pendingRequestManager, pollScheduler, deviceService, serverId); @@ -158,9 +162,9 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { try { // 1. 启动配置刷新定时器 - int refreshInterval = slaveConfig.getConfigRefreshInterval(); + IotModbusTcpSlaveConfig slaveConfig = properties.getModbusTcpSlave(); configRefreshTimerId = vertx.setPeriodic( - TimeUnit.SECONDS.toMillis(refreshInterval), + TimeUnit.SECONDS.toMillis(slaveConfig.getConfigRefreshInterval()), id -> refreshConfig()); // 2.1 启动 TCP Server @@ -178,6 +182,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT Modbus TCP Slave 协议 {} 启动失败]", getId(), e); + // TODO @芋艿:后续统一优化 stop 逻辑; if (configRefreshTimerId != null) { vertx.cancelTimer(configRefreshTimerId); configRefreshTimerId = null; @@ -223,9 +228,9 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { pollScheduler.stopAll(); // 2.3 清理 PendingRequest pendingRequestManager.clear(); - // 2.3 关闭所有连接 + // 2.4 关闭所有连接 connectionManager.closeAll(); - // 2.4 关闭 TCP Server + // 2.5 关闭 TCP Server if (netServer != null) { try { netServer.close().result(); @@ -308,9 +313,6 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { /** * 刷新已连接设备的配置(定时调用) - *

                              - * 与 tcpmaster 不同,slave 只刷新已连接设备的配置,不做全量 diff。 - * 设备的新增(认证时)和删除(断连时)分别在 {@link #handleConnection} 中处理。 */ private synchronized void refreshConfig() { try { @@ -321,6 +323,10 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { } List configs = configCacheService.refreshConnectedDeviceConfigList(connectedDeviceIds); + if (configs == null) { + log.warn("[refreshConfig][刷新配置失败,跳过本次刷新]"); + return; + } log.debug("[refreshConfig][刷新了 {} 个已连接设备的配置]", configs.size()); // 2. 更新已连接设备的轮询任务 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java index b7661abcfc..347b3c5386 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java @@ -29,7 +29,7 @@ public class IotModbusFrame { /** * 事务标识符 *

                              - * 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值, + * 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值 */ private Integer transactionId; @@ -37,14 +37,13 @@ public class IotModbusFrame { * 异常码 *

                              * 当功能码最高位为 1 时(异常响应),此字段存储异常码。 - * 为 null 表示非异常响应。 * * @see IotModbusCommonUtils#FC_EXCEPTION_MASK */ private Integer exceptionCode; /** - * 自定义功能码时的 JSON 字符串 + * 自定义功能码时的 JSON 字符串(用于 auth 认证等等) */ private String customData; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java index fc5219e197..b47f2a11be 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java @@ -20,20 +20,20 @@ import java.util.function.BiConsumer; * 1. 首帧检测:读前 6 字节,判断 MODBUS_TCP(ProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU * 2. 检测后切换到对应的拆包 Handler,并将首包 6 字节通过 handleFirstBytes() 交给新 Handler 处理 * 3. 拆包完成后解码为 IotModbusFrame,通过回调返回 - * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动) - * - MODBUS_RTU:功能码驱动的状态机 + * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动) + * - MODBUS_RTU:功能码驱动的状态机 * * @author 芋道源码 */ +@RequiredArgsConstructor @Slf4j public class IotModbusFrameDecoder { + /** + * 自定义功能码 + */ private final int customFunctionCode; - public IotModbusFrameDecoder(int customFunctionCode) { - this.customFunctionCode = customFunctionCode; - } - /** * 创建带自动帧格式检测的 RecordParser * @@ -82,7 +82,7 @@ public class IotModbusFrameDecoder { // 提取 PDU 数据(从 functionCode 之后到末尾) byte[] pdu = new byte[data.length - 8]; System.arraycopy(data, 8, pdu, 0, pdu.length); - + // 构建 IotModbusFrame return buildFrame(slaveId, functionCode, pdu, transactionId); } @@ -105,7 +105,7 @@ public class IotModbusFrameDecoder { // PDU 数据(不含 slaveId、functionCode、CRC) byte[] pdu = new byte[data.length - 4]; System.arraycopy(data, 2, pdu, 0, pdu.length); - + // 构建 IotModbusFrame return buildFrame(slaveId, functionCode, pdu, null); } @@ -144,7 +144,6 @@ public class IotModbusFrameDecoder { /** * 帧格式检测阶段 Handler(仅处理首包,探测后切换到对应的拆包 Handler) */ - @SuppressWarnings("ClassCanBeRecord") @RequiredArgsConstructor private class DetectPhaseHandler implements Handler { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java index 770f13e3f8..727cc7cea4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java @@ -51,7 +51,7 @@ public class IotModbusTcpSlaveDownstreamHandler { /** * 处理下行消息 */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "DuplicatedCode"}) public void handle(IotDeviceMessage message) { // 1.1 检查是否是属性设置消息 if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { @@ -125,17 +125,26 @@ public class IotModbusTcpSlaveDownstreamHandler { point.getRegisterAddress(), rawValues[0], frameFormat, transactionId); } else if (writeMultipleCode != null) { // 多个值:使用多写功能码(FC15/FC16) - data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId, - point.getRegisterAddress(), rawValues, frameFormat, transactionId); + if (writeMultipleCode == IotModbusCommonUtils.FC_WRITE_MULTIPLE_COILS) { + data = frameEncoder.encodeWriteMultipleCoilsRequest(slaveId, + point.getRegisterAddress(), rawValues, frameFormat, transactionId); + } else { + data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId, + point.getRegisterAddress(), rawValues, frameFormat, transactionId); + } } else { log.warn("[writeProperty][点位 {} 不支持写操作, 功能码={}]", point.getIdentifier(), readFunctionCode); return; } - // 2. 发送 - connectionManager.sendToDevice(deviceId, data); - log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", - deviceId, point.getIdentifier(), value); + // 2. 发送消息 + connectionManager.sendToDevice(deviceId, data).onSuccess(v -> + log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", + deviceId, point.getIdentifier(), value) + ).onFailure(e -> + log.error("[writeProperty][写入失败, deviceId={}, identifier={}, value={}]", + deviceId, point.getIdentifier(), value, e) + ); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java index 6ede74c1db..4742610de9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java @@ -1,9 +1,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream; -import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; @@ -61,6 +61,7 @@ public class IotModbusTcpSlaveUpstreamHandler { private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; private final IotModbusTcpSlavePollScheduler pollScheduler; private final IotDeviceService deviceService; + private final String serverId; public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi, @@ -153,16 +154,20 @@ public class IotModbusTcpSlaveUpstreamHandler { // 1. 解析认证参数 IotDeviceAuthReqDTO request = JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); Assert.notNull(request, "认证参数不能为空"); - Assert.notBlank(request.getClientId(), "clientId 不能为空"); Assert.notBlank(request.getUsername(), "username 不能为空"); Assert.notBlank(request.getPassword(), "password 不能为空"); + // 特殊:考虑到 modbus 消息体积较小,默认 clientId 传递空串 + if (StrUtil.isBlank(request.getClientId())) { + request.setClientId(IotDeviceAuthUtils.buildClientIdFromUsername(request.getUsername())); + } + Assert.notBlank(request.getClientId(), "clientId 不能为空"); // 2.1 调用认证 API CommonResult result = deviceApi.authDevice(request); result.checkError(); if (BooleanUtil.isFalse(result.getData())) { log.warn("[handleAuth][认证失败, clientId={}, username={}]", request.getClientId(), request.getUsername()); - sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, 1, "认证失败"); + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), "认证失败"); return; } // 2.2 解析设备信息 @@ -171,7 +176,21 @@ public class IotModbusTcpSlaveUpstreamHandler { // 2.3 获取设备信息 IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); Assert.notNull(device, "设备不存在"); - // TODO @AI:2.4 必须找到连接配置; + // 2.4 加载设备 Modbus 配置,无配置则阻断认证 + IotModbusDeviceConfigRespDTO modbusConfig = configCacheService.loadDeviceConfig(device.getId()); + if (modbusConfig == null) { + log.warn("[handleAuth][设备 {} 没有 Modbus 点位配置, 拒绝认证]", device.getId()); + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), "设备无 Modbus 配置"); + return; + } + // 2.5 协议不一致,阻断认证 + if (ObjUtil.notEqual(frameFormat.getFormat(), modbusConfig.getFrameFormat())) { + log.warn("[handleAuth][设备 {} frameFormat 不一致, 连接协议={}, 设备配置={},拒绝认证]", + device.getId(), frameFormat.getFormat(), modbusConfig.getFrameFormat()); + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), + "frameFormat 协议不一致"); + return; + } // 3.1 注册连接 ConnectionInfo connectionInfo = new ConnectionInfo() @@ -189,13 +208,8 @@ public class IotModbusTcpSlaveUpstreamHandler { GlobalErrorCodeConstants.SUCCESS.getCode(), "success"); log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", request.getClientId(), device.getId()); - // 4. 加载设备配置并启动轮询 - IotModbusDeviceConfigRespDTO config = configCacheService.loadDeviceConfig(device.getId()); - if (config != null) { - pollScheduler.updatePolling(config); - } else { - log.warn("[handleAuth][认证成功但未找到设备配置, deviceId={}]", device.getId()); - } + // 4. 启动轮询 + pollScheduler.updatePolling(modbusConfig); } /** @@ -245,20 +259,19 @@ public class IotModbusTcpSlaveUpstreamHandler { } // 2.3 查找点位配置 IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId()); - if (config == null || CollUtil.isEmpty(config.getPoints())) { - return; - } IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, request.getPointId()); if (point == null) { return; } - // 3.1 点位翻译 + // 3.1 转换原始值为物模型属性值(点位翻译) Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValues, point); - // 3.2 上报属性 + // 3.2 构造属性上报消息 Map params = MapUtil.of(request.getIdentifier(), convertedValue); IotDeviceMessage message = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); + + // 4. 发送到消息总线 messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId); log.debug("[handlePollingResponse][设备={}, 属性={}, 原始值={}, 转换值={}]", info.getDeviceId(), request.getIdentifier(), rawValues, convertedValue); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java index 9a00997f05..a998a77079 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java @@ -1,10 +1,15 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -13,16 +18,7 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** - * IoT Modbus TCP Slave 配置缓存服务 - *

                              - * 与 tcpmaster 的 {@code IotModbusTcpConfigCacheService} 不同: - * - tcpmaster 启动时拉全量配置 → 主动建连,需要全量 diff 检测新增/删除设备 - * - tcpslave 设备主动连接 → 认证时按需加载配置,断连时清理,定时刷新已连接设备的配置 - *

                              - * 配置生命周期: - * 1. 认证时:{@link #loadDeviceConfig(Long)} 按 deviceId 从 API 加载配置到缓存 - * 2. 断连时:{@link #removeConfig(Long)} 从缓存中移除 - * 3. 定时刷新:{@link #refreshConnectedDeviceConfigList(Set)} 批量刷新已连接设备的配置 + * IoT Modbus TCP Slave 配置缓存:认证时按需加载,断连时清理,定时刷新已连接设备 * * @author 芋道源码 */ @@ -41,44 +37,33 @@ public class IotModbusTcpSlaveConfigCacheService { /** * 加载单个设备的配置(认证成功后调用) - *

                              - * 从远程 API 获取全量配置,然后按 deviceId 匹配。 - * 如果远程获取失败,尝试从 Mock 数据中匹配。 * * @param deviceId 设备 ID - * @return 设备配置,未找到返回 null + * @return 设备配置 */ public IotModbusDeviceConfigRespDTO loadDeviceConfig(Long deviceId) { try { - // 1. 从远程 API 获取全量配置 - // TODO @AI:等待修复,不着急; - CommonResult> result = deviceApi.getEnabledModbusDeviceConfigs(); - if (result != null && result.isSuccess() && result.getData() != null) { - for (IotModbusDeviceConfigRespDTO config : result.getData()) { - // 顺便更新缓存(其他已连接设备也受益) - configCache.put(config.getDeviceId(), config); - if (config.getDeviceId().equals(deviceId)) { - return config; - } - } + // 1. 从远程 API 获取配置 + IotModbusDeviceConfigListReqDTO reqDTO = new IotModbusDeviceConfigListReqDTO() + .setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setMode(IotModbusModeEnum.POLLING.getMode()) + .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SLAVE.getType()) + .setDeviceIds(Collections.singleton(deviceId)); + CommonResult> result = deviceApi.getModbusDeviceConfigList(reqDTO); + result.checkError(); + IotModbusDeviceConfigRespDTO modbusConfig = CollUtil.getFirst(result.getData()); + if (modbusConfig == null) { + log.warn("[loadDeviceConfig][远程获取配置失败,未找到数据, deviceId={}]", deviceId); + return null; } + + // 2. 更新缓存并返回 + configCache.put(modbusConfig.getDeviceId(), modbusConfig); + return modbusConfig; } catch (Exception e) { log.error("[loadDeviceConfig][从远程获取配置失败, deviceId={}]", deviceId, e); + return null; } - - // 2. 远程未找到,尝试 Mock 数据(仅 mockEnabled=true 时) - // DONE @AI:【from codex】【中】Mock 数据已通过 mockEnabled 配置开关控制,线上环境不会污染真实配置。 - // TODO @芋艿:测试完成后移除 - if (true) { - for (IotModbusDeviceConfigRespDTO mockConfig : buildMockConfigs()) { - configCache.put(mockConfig.getDeviceId(), mockConfig); - if (mockConfig.getDeviceId().equals(deviceId)) { - return mockConfig; - } - } - } - - return configCache.get(deviceId); } // ==================== 定时刷新(已连接设备) ==================== @@ -96,26 +81,25 @@ public class IotModbusTcpSlaveConfigCacheService { return Collections.emptyList(); } try { - // 1. 从远程获取全量配置 - // TODO @AI:传递 ids 批量查询;需要分批啦; - CommonResult> result = deviceApi.getEnabledModbusDeviceConfigs(); - List allConfigs; - if (result != null && result.isSuccess() && result.getData() != null) { - allConfigs = new ArrayList<>(result.getData()); - } else { - log.warn("[refreshConnectedDeviceConfigList][获取 Modbus 配置失败: {}]", result); - allConfigs = new ArrayList<>(); - } + // 1. 从远程获取已连接设备的配置 + CommonResult> result = deviceApi.getModbusDeviceConfigList( + new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setMode(IotModbusModeEnum.POLLING.getMode()) + .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SLAVE.getType()) + .setDeviceIds(connectedDeviceIds)); + List modbusConfigs = result.getCheckedData(); // 2. 追加 Mock 测试数据(仅 mockEnabled=true 时) // TODO @芋艿:测试完成后移除 + // TODO @claude-code:【严重】同上,if(true) 导致 mockEnabled 开关失效,Mock 数据永远加载 if (true) { - allConfigs.addAll(buildMockConfigs()); + modbusConfigs.addAll(buildMockConfigs()); } - // 3. 只保留已连接设备的配置,更新缓存 + // 2. 只保留已连接设备的配置,更新缓存 + // TODO @AI:是不是直接添加到 configCache 缓存(或者覆盖),然后返回 modbusConfigs 就 ok 了?! List connectedConfigs = new ArrayList<>(); - for (IotModbusDeviceConfigRespDTO config : allConfigs) { + for (IotModbusDeviceConfigRespDTO config : modbusConfigs) { if (connectedDeviceIds.contains(config.getDeviceId())) { configCache.put(config.getDeviceId(), config); connectedConfigs.add(config); @@ -124,15 +108,7 @@ public class IotModbusTcpSlaveConfigCacheService { return connectedConfigs; } catch (Exception e) { log.error("[refreshConnectedDeviceConfigList][刷新配置失败]", e); - // 降级:返回缓存中已连接设备的配置 - List fallback = new ArrayList<>(); - for (Long deviceId : connectedDeviceIds) { - IotModbusDeviceConfigRespDTO config = configCache.get(deviceId); - if (config != null) { - fallback.add(config); - } - } - return fallback; + return null; } } @@ -142,7 +118,12 @@ public class IotModbusTcpSlaveConfigCacheService { * 获取设备配置 */ public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { - return configCache.get(deviceId); + IotModbusDeviceConfigRespDTO config = configCache.get(deviceId); + if (config != null) { + return config; + } + // 缓存未命中,从远程 API 获取 + return loadDeviceConfig(deviceId); } /** @@ -169,7 +150,7 @@ public class IotModbusTcpSlaveConfigCacheService { config.setDeviceName("small"); config.setSlaveId(1); config.setMode(1); // 云端轮询 - config.setFrameFormat("modbus_tcp"); + config.setFrameFormat(IotModbusFrameFormatEnum.MODBUS_TCP.getFormat()); // 点位列表 List points = new ArrayList<>(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java index 1a6a4cc610..04434910aa 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import io.vertx.core.Future; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; import lombok.Data; @@ -129,21 +130,25 @@ public class IotModbusTcpSlaveConnectionManager { /** * 发送数据到设备 + * + * @return 发送结果 Future */ - public void sendToDevice(Long deviceId, byte[] data) { + public Future sendToDevice(Long deviceId, byte[] data) { NetSocket socket = deviceSocketMap.get(deviceId); if (socket == null) { log.warn("[sendToDevice][设备 {} 没有连接]", deviceId); - return; + return Future.failedFuture("设备 " + deviceId + " 没有连接"); } - sendToSocket(socket, data); + return sendToSocket(socket, data); } /** * 发送数据到指定 socket + * + * @return 发送结果 Future */ - public void sendToSocket(NetSocket socket, byte[] data) { - socket.write(Buffer.buffer(data)); + public Future sendToSocket(NetSocket socket, byte[] data) { + return socket.write(Buffer.buffer(data)); } /** diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java index 8ddcd25598..a02601d666 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; import lombok.AllArgsConstructor; import lombok.Data; @@ -76,8 +77,9 @@ public class IotModbusTcpSlavePendingRequestManager { if (frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP && frame.getTransactionId() != null) { return matchByTransactionId(queue, frame.getTransactionId()); } - // RTU 模式:FIFO,匹配 slaveId + functionCode - return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode()); + // RTU 模式:FIFO,匹配 slaveId + functionCode + registerCount + int responseRegisterCount = extractRegisterCountFromResponse(frame); + return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode(), responseRegisterCount); } /** @@ -96,13 +98,16 @@ public class IotModbusTcpSlavePendingRequestManager { } /** - * 按 FIFO 匹配 + * 按 FIFO 匹配(slaveId + functionCode + registerCount) */ - private PendingRequest matchByFifo(Deque queue, int slaveId, int functionCode) { + private PendingRequest matchByFifo(Deque queue, int slaveId, int functionCode, + int responseRegisterCount) { Iterator it = queue.iterator(); while (it.hasNext()) { PendingRequest req = it.next(); - if (req.getSlaveId() == slaveId && req.getFunctionCode() == functionCode) { + if (req.getSlaveId() == slaveId + && req.getFunctionCode() == functionCode + && (responseRegisterCount <= 0 || req.getRegisterCount() == responseRegisterCount)) { it.remove(); return req; } @@ -110,6 +115,29 @@ public class IotModbusTcpSlavePendingRequestManager { return null; } + // TODO @AI:是不是放到 modbus 工具类里,更合适? + /** + * 从响应帧中提取 registerCount(通过 PDU 的 byteCount 推断) + * + * @return registerCount,无法提取时返回 -1(匹配时跳过校验) + */ + private int extractRegisterCountFromResponse(IotModbusFrame frame) { + byte[] pdu = frame.getPdu(); + if (pdu == null || pdu.length < 1) { + return -1; + } + int byteCount = pdu[0] & 0xFF; + int fc = frame.getFunctionCode(); + // FC03/04 寄存器读响应:registerCount = byteCount / 2 + if (fc == IotModbusCommonUtils.FC_READ_HOLDING_REGISTERS + || fc == IotModbusCommonUtils.FC_READ_INPUT_REGISTERS) { + return byteCount / 2; + } + // FC01/02 线圈/离散输入读响应:registerCount = byteCount * 8(线圈数量) + // 但因为按 bit 打包有余位,无法精确反推,返回 -1 跳过校验 + return -1; + } + /** * 清理过期请求 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java index 8a9cba5963..f1276eaa52 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java @@ -55,6 +55,7 @@ public class IotModbusTcpSlavePollScheduler extends AbstractIotModbusPollSchedul * 轮询单个点位 */ @Override + @SuppressWarnings("DuplicatedCode") protected void pollPoint(Long deviceId, Long pointId) { // 1.1 从 configCache 获取最新配置 IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); @@ -78,7 +79,7 @@ public class IotModbusTcpSlavePollScheduler extends AbstractIotModbusPollSchedul // 2.2 获取 slave ID IotModbusFrameFormatEnum frameFormat = connection.getFrameFormat(); Assert.notNull(frameFormat, "设备 {} 的帧格式不能为空", deviceId); - int slaveId = connection.getSlaveId(); + Integer slaveId = connection.getSlaveId(); Assert.notNull(connection.getSlaveId(), "设备 {} 的 slaveId 不能为空", deviceId); // 3.1 编码读请求 @@ -96,10 +97,13 @@ public class IotModbusTcpSlavePollScheduler extends AbstractIotModbusPollSchedul System.currentTimeMillis() + requestTimeout); pendingRequestManager.addRequest(pendingRequest); // 3.3 发送读请求 - connectionManager.sendToDevice(deviceId, data); - log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]", - deviceId, point.getIdentifier(), point.getFunctionCode(), - point.getRegisterAddress(), point.getRegisterCount()); + connectionManager.sendToDevice(deviceId, data).onSuccess(v -> + log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]", + deviceId, point.getIdentifier(), point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount()) + ).onFailure(e -> + log.warn("[pollPoint][发送失败, 设备={}, 点位={}]", deviceId, point.getIdentifier(), e) + ); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/package-info.java new file mode 100644 index 0000000000..cd8f1cb322 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/package-info.java @@ -0,0 +1,6 @@ +/** + * Modbus TCP Slave(从站)协议:设备主动连接网关,自定义 FC65 认证后由网关云端轮询 + *

                              + * TCP Server 模式,支持 MODBUS_TCP / MODBUS_RTU 帧格式自动检测 + */ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java index dfda30db40..702876db91 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; @@ -64,8 +65,8 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { } @Override - public CommonResult> getEnabledModbusDeviceConfigs() { - return doPost("/rpc-api/iot/modbus/enabled-configs", null, new ParameterizedTypeReference<>() { }); + public CommonResult> getModbusDeviceConfigList(IotModbusDeviceConfigListReqDTO listReqDTO) { + return doPost("/rpc-api/iot/modbus/config-list", listReqDTO, new ParameterizedTypeReference<>() { }); } @Override diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index e2cfd94abb..1ccd9f37b3 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -167,7 +167,7 @@ yudao: # 针对引入的 Modbus TCP Master 组件的配置 # ==================================== - id: modbus-tcp-master-1 - enabled: false + enabled: true protocol: modbus_tcp_master port: 502 modbus-tcp-master: diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java index c81ab4a83a..b8c1edb8a0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java @@ -67,9 +67,9 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; - private static final String DEVICE_NAME = "small"; - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; + private static final String PRODUCT_KEY = "modbus_tcp_slave_product_demo"; + private static final String DEVICE_NAME = "modbus_tcp_slave_device_demo_tcp"; + private static final String DEVICE_SECRET = "8e4adeb3d25342ab88643421d3fba3f6"; @BeforeAll static void setUp() { @@ -128,7 +128,6 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { // 2. 设置持续监听:每收到一个读请求,自动回复 log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); - CompletableFuture done = new CompletableFuture<>(); RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> { log.info("[testPollingResponse][收到请求: slaveId={}, FC={}, transactionId={}]", frame.getSlaveId(), frame.getFunctionCode(), frame.getTransactionId()); @@ -201,6 +200,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { */ private IotModbusFrame authenticate(NetSocket socket) throws Exception { IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + authInfo.setClientId(""); // 特殊:考虑到 modbus 消息长度限制,默认 clientId 不发送 byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); return sendAndReceive(socket, authFrame); } From 88f090b66f2c9cc2cb151dbfe9f3275b237df85c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 8 Feb 2026 23:11:38 +0800 Subject: [PATCH 40/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9Amodbus-?= =?UTF-8?q?tcp-slave=E3=80=81modbus-tcp-master=20=E6=8E=A5=E7=9D=80?= =?UTF-8?q?=E5=A4=A7=E9=87=8F=E4=BC=98=E5=8C=96=EF=BC=8C=E5=B9=B6=E4=B8=94?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20modbus=20rtu=20=E7=BC=96=E8=A7=A3=E7=A0=81?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IotDeviceModbusConfigSaveReqVO.java | 4 - .../device/IotDeviceModbusPointDO.java | 8 +- .../device/IotDeviceModbusPointMapper.java | 5 ++ .../iot/enums/rule/IotDataSinkTypeEnum.java | 4 +- .../device/IotDeviceMessageSubscriber.java | 1 - .../rule/IotSceneRuleMessageHandler.java | 1 - .../IotDeviceModbusConfigServiceImpl.java | 33 ++++++- .../device/IotDeviceModbusPointService.java | 9 ++ .../IotDeviceModbusPointServiceImpl.java | 7 +- .../action/IotDataRuleCacheableAction.java | 2 - .../thingmodel/IotThingModelServiceImpl.java | 10 ++- .../common/utils/IotModbusCommonUtils.java | 21 +++++ .../tcpmaster/IotModbusTcpMasterProtocol.java | 1 - .../IotModbusTcpMasterConfigCacheService.java | 2 - .../IotModbusTcpMasterConnectionManager.java | 31 ++++--- .../tcpslave/IotModbusTcpSlaveConfig.java | 6 -- .../tcpslave/codec/IotModbusFrameDecoder.java | 43 ++++++++- .../tcpslave/codec/IotModbusFrameEncoder.java | 3 - .../IotModbusTcpSlaveDownstreamHandler.java | 1 - .../IotModbusTcpSlaveConfigCacheService.java | 87 ++----------------- ...otModbusTcpSlavePendingRequestManager.java | 25 +----- .../upstream/IotMqttUpstreamHandler.java | 6 ++ .../upstream/IotUdpUpstreamHandler.java | 1 - .../iot/gateway/util/IotMqttTopicUtils.java | 2 - ...=> IoTModbusTcpMasterIntegrationTest.java} | 62 ++++++++----- ... IotModbusTcpSlaveRtuIntegrationTest.java} | 36 ++++---- ... IotModbusTcpSlaveTcpIntegrationTest.java} | 26 ++---- 27 files changed, 224 insertions(+), 213 deletions(-) rename yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/{ModbusTcpSlaveSimulatorTest.java => IoTModbusTcpMasterIntegrationTest.java} (53%) rename yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/{IotModbusTcpSlaveModbusRtuIntegrationTest.java => IotModbusTcpSlaveRtuIntegrationTest.java} (93%) rename yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/{IotModbusTcpSlaveModbusTcpIntegrationTest.java => IotModbusTcpSlaveTcpIntegrationTest.java} (95%) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java index 9fa3fdc7c2..9b08d88555 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java @@ -16,13 +16,9 @@ public class IotDeviceModbusConfigSaveReqVO { private Long deviceId; @Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100") -// @NotEmpty(message = "Modbus 服务器 IP 地址不能为空") - // TODO @AI:这个字段,要根据情况校验; private String ip; @Schema(description = "Modbus 端口", example = "502") -// @NotNull(message = "Modbus 端口不能为空") - // TODO @AI:这个字段,要根据情况校验; private Integer port; @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java index dd3a3a9609..2937476cff 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java @@ -29,7 +29,12 @@ public class IotDeviceModbusPointDO extends TenantBaseDO { */ @TableId private Long id; - // TODO @AI:增加 productId; + /** + * 产品编号 + * + * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} + */ + private Long productId; /** * 设备编号 * @@ -42,7 +47,6 @@ public class IotDeviceModbusPointDO extends TenantBaseDO { * 关联 {@link IotThingModelDO#getId()} */ private Long thingModelId; - // TODO @AI:每次物模型的变更时,需要按需刷下 identifier、name 配置; /** * 属性标识符 * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java index 74dc463e41..7c9b5d3bae 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java @@ -39,4 +39,9 @@ public interface IotDeviceModbusPointMapper extends BaseMapperX() + .eq(IotDeviceModbusPointDO::getThingModelId, thingModelId)); + } + } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java index 440fab5f53..96b477d69d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java +++ b/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 { TCP(2, "TCP"), WEBSOCKET(3, "WebSocket"), - MQTT(10, "MQTT"), // TODO 待实现; + MQTT(10, "MQTT"), // TODO @puhui999:待实现; - DATABASE(20, "Database"), // TODO @puhui999:待实现;可以简单点,对应的表名是什么,字段先固定了。 + DATABASE(20, "Database"), // TODO @puhui999:待实现; REDIS(21, "Redis"), ROCKETMQ(30, "RocketMQ"), diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java index 7e039d0327..c6e0ba4221 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java @@ -67,7 +67,6 @@ public class IotDeviceMessageSubscriber implements IotMessageSubscriber getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO); + /** + * 物模型变更时,更新关联点位的冗余字段(identifier、name) + * + * @param thingModelId 物模型编号 + * @param identifier 物模型标识符 + * @param name 物模型名称 + */ + void updateDeviceModbusPointByThingModel(Long thingModelId, String identifier, String name); + /** * 根据设备编号批量获得启用的点位配置 Map * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java index 906697b15b..7683aa7ecc 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java @@ -75,7 +75,12 @@ public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointServ modbusPointMapper.updateById(updateObj); } - // TODO @AI:物模型更新的时候,更新下 identifier、name 信息;例如说 updateDeviceModbusPoint(thingModelId, identifier、name) 方法; + @Override + public void updateDeviceModbusPointByThingModel(Long thingModelId, String identifier, String name) { + IotDeviceModbusPointDO updateObj = new IotDeviceModbusPointDO() + .setIdentifier(identifier).setName(name); + modbusPointMapper.updateByThingModelId(thingModelId, updateObj); + } private IotThingModelDO validateThingModelExists(Long id) { IotThingModelDO thingModel = thingModelService.getThingModel(id); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java index 4319469082..cc282e1b8d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java +++ b/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; // TODO @芋艿:数据库 // TODO @芋艿:mqtt -// TODO @芋艿:tcp -// TODO @芋艿:websocket /** * 可缓存的 {@link IotDataRuleAction} 抽象实现 diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java index ca04ecd5f3..18a5b62e2a 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java +++ b/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; import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper; import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -51,6 +52,9 @@ public class IotThingModelServiceImpl implements IotThingModelService { @Lazy // 延迟加载,解决循环依赖 private IotProductService productService; + @Resource + private IotDeviceModbusPointService deviceModbusPointService; + @Override @Transactional(rollbackFor = Exception.class) public Long createThingModel(IotThingModelSaveReqVO createReqVO) { @@ -84,7 +88,11 @@ public class IotThingModelServiceImpl implements IotThingModelService { IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO); thingModelMapper.updateById(thingModel); - // 3. 删除缓存 + // 3. 同步更新 Modbus 点位的冗余字段(identifier、name) + deviceModbusPointService.updateDeviceModbusPointByThingModel( + updateReqVO.getId(), updateReqVO.getIdentifier(), updateReqVO.getName()); + + // 4. 删除缓存 deleteThingModelListCache(updateReqVO.getProductId()); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java index 23ee4bf124..32e7292964 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java @@ -500,6 +500,27 @@ public class IotModbusCommonUtils { return values; } + /** + * 从响应帧中提取 registerCount(通过 PDU 的 byteCount 推断) + * + * @param frame 解码后的 Modbus 响应帧 + * @return registerCount,无法提取时返回 -1(匹配时跳过校验) + */ + public static int extractRegisterCountFromResponse(IotModbusFrame frame) { + byte[] pdu = frame.getPdu(); + if (pdu == null || pdu.length < 1) { + return -1; + } + int byteCount = pdu[0] & 0xFF; + int fc = frame.getFunctionCode(); + // FC03/04 寄存器读响应:registerCount = byteCount / 2 + if (fc == FC_READ_HOLDING_REGISTERS || fc == FC_READ_INPUT_REGISTERS) { + return byteCount / 2; + } + // FC01/02 线圈/离散输入读响应:按 bit 打包有余位,无法精确反推,返回 -1 跳过校验 + return -1; + } + // ==================== 点位查找 ==================== /** diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java index 1fac973a92..1b284e4e4c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java @@ -84,7 +84,6 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); this.configCacheService = new IotModbusTcpMasterConfigCacheService(deviceApi); - // DONE @AI:上线/下线消息已移到 ConnectionManager 内部处理,不再走回调 this.connectionManager = new IotModbusTcpMasterConnectionManager(redissonClient, vertx, messageService, configCacheService, serverId); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java index 7ce6dd02fc..aa6273485f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java @@ -79,8 +79,6 @@ public class IotModbusTcpMasterConfigCacheService { /** * 计算已删除设备的 ID 集合,清理缓存,并更新已知设备 ID 集合 * - * DONE @AI:不再使用 callback 模式,返回已删除的设备 ID 集合,由调用方直接清理 - * * @param currentConfigs 当前有效的配置列表 * @return 已删除的设备 ID 集合 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java index 34b68f65b6..ebbca8e2a0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java @@ -96,18 +96,19 @@ public class IotModbusTcpMasterConnectionManager { connection = connectionPool.get(connectionKey); if (connection != null) { addDeviceAndOnline(connection, config); + lock.unlock(); return; } // 3.2 创建新连接 connection = createConnection(config); + connection.setLock(lock); connectionPool.put(connectionKey, connection); log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey); // 3.3 注册设备并发送上线消息 addDeviceAndOnline(connection, config); } catch (Exception e) { log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e); - } finally { - // TODO @AI:如果这里释放,会不会出现,集群模式下,多个节点同时创建连接的情况?需要验证一下 Redisson 的分布式锁特性?! + // 建连失败,释放锁让其他节点可重试 lock.unlock(); } } @@ -231,11 +232,11 @@ public class IotModbusTcpMasterConnectionManager { if (connection.getTcpConnection() != null) { connection.getTcpConnection().close(); } - // 强制解锁,避免死锁(正常情况下应该不会发生锁未释放的情况) -// RLock lock = connection.getLock(); -// if (lock != null && lock.isLocked()) { -// lock.forceUnlock(); -// } + // 释放分布式锁,让其他节点可接管 + RLock lock = connection.getLock(); + if (lock != null && lock.isHeldByCurrentThread()) { + lock.unlock(); + } log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey); } catch (Exception e) { log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e); @@ -266,17 +267,21 @@ public class IotModbusTcpMasterConnectionManager { private TCPMasterConnection tcpConnection; private Integer timeout; private Integer retryInterval; - - private Context context; - - // TODO @AI:是不是需要 lock?!避免集群模式下的竞争(肯定不能让别的节点连接上)!!!【另外,RLock 在节点(持有所锁的节点) cransh 的时候,会自动释放】 -// private RLock lock; - /** * 设备 ID 到 slave ID 的映射 */ private final Map deviceSlaveMap = new ConcurrentHashMap<>(); + /** + * 分布式锁,锁住连接的创建和销毁,避免多节点重复连接同一从站 + */ + private RLock lock; + + /** + * Vert.x Context,用于 executeBlocking 执行 Modbus 操作,保证同一连接的操作串行执行 + */ + private Context context; + public void addDevice(Long deviceId, Integer slaveId) { deviceSlaveMap.put(deviceId, slaveId); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java index d21c28e676..60185f1eb0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java @@ -41,10 +41,4 @@ public class IotModbusTcpSlaveConfig { @NotNull(message = "请求清理间隔不能为空") private Integer requestCleanupInterval = 10000; - // TODO @AI:可以去掉这个开关,因为本身就是模拟的,稍后我自己也会手动或者让你去掉(听我指令!) - /** - * 是否启用 Mock 测试数据(仅开发/测试环境使用,线上务必关闭) - */ - private Boolean mockEnabled = false; - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java index b47f2a11be..1415b51779 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java @@ -29,21 +29,36 @@ import java.util.function.BiConsumer; @Slf4j public class IotModbusFrameDecoder { + private static final Boolean REQUEST_MODE_DEFAULT = false; + /** * 自定义功能码 */ private final int customFunctionCode; /** - * 创建带自动帧格式检测的 RecordParser + * 创建带自动帧格式检测的 RecordParser(默认响应模式) * * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式) * @return RecordParser 实例 */ public RecordParser createRecordParser(BiConsumer frameHandler) { + return createRecordParser(frameHandler, REQUEST_MODE_DEFAULT); + } + + /** + * 创建带自动帧格式检测的 RecordParser + * + * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式) + * @param requestMode 是否为请求模式(true:接收方收到的是 Modbus 请求帧,FC01-04 按固定 8 字节解析; + * false:接收方收到的是 Modbus 响应帧,FC01-04 按 byteCount 变长解析) + * @return RecordParser 实例 + */ + public RecordParser createRecordParser(BiConsumer frameHandler, + boolean requestMode) { // 先创建一个 RecordParser:使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测 RecordParser parser = RecordParser.newFixed(6); - parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler)); + parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler, requestMode)); return parser; } @@ -150,6 +165,7 @@ public class IotModbusFrameDecoder { private final RecordParser parser; private final int customFunctionCode; private final BiConsumer frameHandler; + private final boolean requestMode; @Override public void handle(Buffer buffer) { @@ -169,7 +185,7 @@ public class IotModbusFrameDecoder { } else { // MODBUS_RTU:切换到 RTU 拆包 Handler log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]"); - RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, frameHandler, customFunctionCode); + RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, frameHandler, customFunctionCode, requestMode); parser.handler(rtuHandler); // 当前 bytes 包含前 6 字节(slaveId + FC + 部分数据),交给 rtuHandler 处理 rtuHandler.handleFirstBytes(bytes); @@ -248,6 +264,9 @@ public class IotModbusFrameDecoder { * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2) * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2) * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2) + *

                              + * 请求模式(requestMode=true)时,FC01-04 按固定 8 字节解析(与写响应相同路径), + * 因为读请求格式为 [SlaveId(1)][FC(1)][StartAddr(2)][Quantity(2)][CRC(2)] */ @RequiredArgsConstructor private class RtuFrameHandler implements Handler { @@ -261,6 +280,12 @@ public class IotModbusFrameDecoder { private final RecordParser parser; private final BiConsumer frameHandler; private final int customFunctionCode; + /** + * 请求模式: + * - true 表示接收方收到的是 Modbus 请求帧(如设备端收到网关下发的读请求),FC01-04 按固定 8 字节帧解析 + * - false 表示接收方收到的是 Modbus 响应帧,FC01-04 按 byteCount 变长解析 + */ + private final boolean requestMode; private int state = STATE_HEADER; private byte slaveId; @@ -289,6 +314,13 @@ public class IotModbusFrameDecoder { frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC emitFrame(frame); resetToHeader(); + } else if (IotModbusCommonUtils.isReadResponse(fc) && requestMode) { + // 请求模式下的读请求:固定 8 字节 [SlaveId(1)][FC(1)][StartAddr(2)][Quantity(2)][CRC(2)] + // 已有 6 字节,还需 2 字节(CRC) + state = STATE_WRITE_BODY; + this.pendingData = Buffer.buffer(); + this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节(StartAddr + Quantity) + parser.fixedSizeMode(2); // 还需 2 字节(CRC) } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) { // 读响应或自定义 FC:bytes[2] = byteCount this.byteCount = bytes[2]; @@ -359,6 +391,11 @@ public class IotModbusFrameDecoder { // 异常响应 state = STATE_EXCEPTION_BODY; parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2) + } else if (IotModbusCommonUtils.isReadResponse(fc) && requestMode) { + // 请求模式下的读请求:固定 8 字节,已读 2 字节(slaveId + FC),还需 6 字节 + state = STATE_WRITE_BODY; + pendingData = Buffer.buffer(); + parser.fixedSizeMode(6); // StartAddr(2) + Quantity(2) + CRC(2) } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) { // 读响应或自定义 FC state = STATE_READ_BYTE_COUNT; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java index 36323cbda5..2a2bb3574a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java @@ -46,9 +46,6 @@ public class IotModbusFrameEncoder { /** * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05) * - * DONE @AI:【from codex】【高】FC05 写线圈时,value 已转换为 Modbus 标准值(非0 → 0xFF00,0 → 0x0000); - * 新增 encodeWriteMultipleCoilsRequest 方法用于 FC15 编码(按 bit 打包)。 - * * @param slaveId 从站地址 * @param functionCode 功能码 * @param address 寄存器地址 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java index 727cc7cea4..900b01a47c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java @@ -109,7 +109,6 @@ public class IotModbusTcpSlaveDownstreamHandler { // 1.2 确定帧格式和事务 ID IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); Assert.notNull(frameFormat, "连接帧格式不能为空"); - // TODO @AI:【from 芋艿】需要和按照 deviceId 进行自增么??? Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP ? (transactionIdCounter.incrementAndGet() & 0xFFFF) : null; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java index a998a77079..cf8589861b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java @@ -6,15 +6,15 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.math.BigDecimal; -import java.util.*; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** @@ -33,8 +33,6 @@ public class IotModbusTcpSlaveConfigCacheService { */ private final Map configCache = new ConcurrentHashMap<>(); - // ==================== 按需加载(认证时) ==================== - /** * 加载单个设备的配置(认证成功后调用) * @@ -66,8 +64,6 @@ public class IotModbusTcpSlaveConfigCacheService { } } - // ==================== 定时刷新(已连接设备) ==================== - /** * 刷新已连接设备的配置缓存 *

                              @@ -89,31 +85,17 @@ public class IotModbusTcpSlaveConfigCacheService { .setDeviceIds(connectedDeviceIds)); List modbusConfigs = result.getCheckedData(); - // 2. 追加 Mock 测试数据(仅 mockEnabled=true 时) - // TODO @芋艿:测试完成后移除 - // TODO @claude-code:【严重】同上,if(true) 导致 mockEnabled 开关失效,Mock 数据永远加载 - if (true) { - modbusConfigs.addAll(buildMockConfigs()); - } - - // 2. 只保留已连接设备的配置,更新缓存 - // TODO @AI:是不是直接添加到 configCache 缓存(或者覆盖),然后返回 modbusConfigs 就 ok 了?! - List connectedConfigs = new ArrayList<>(); + // 2. 更新缓存并返回 for (IotModbusDeviceConfigRespDTO config : modbusConfigs) { - if (connectedDeviceIds.contains(config.getDeviceId())) { - configCache.put(config.getDeviceId(), config); - connectedConfigs.add(config); - } + configCache.put(config.getDeviceId(), config); } - return connectedConfigs; + return modbusConfigs; } catch (Exception e) { log.error("[refreshConnectedDeviceConfigList][刷新配置失败]", e); return null; } } - // ==================== 缓存操作 ==================== - /** * 获取设备配置 */ @@ -133,59 +115,4 @@ public class IotModbusTcpSlaveConfigCacheService { configCache.remove(deviceId); } - // ==================== Mock 数据 ==================== - - /** - * 构建 Mock 测试配置数据(一次性测试用途) - * - * 设备:PRODUCT_KEY=4aymZgOTOOCrDKRT, DEVICE_NAME=small - * 点位:temperature(FC03, 地址 0)、humidity(FC03, 地址 1) - * - * TODO @芋艿:测试完成后移除 - */ - private List buildMockConfigs() { - IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO(); - config.setDeviceId(25L); - config.setProductKey("4aymZgOTOOCrDKRT"); - config.setDeviceName("small"); - config.setSlaveId(1); - config.setMode(1); // 云端轮询 - config.setFrameFormat(IotModbusFrameFormatEnum.MODBUS_TCP.getFormat()); - - // 点位列表 - List points = new ArrayList<>(); - - // 点位 1:温度 - 保持寄存器 FC03, 地址 0, 1 个寄存器, INT16 - IotModbusPointRespDTO point1 = new IotModbusPointRespDTO(); - point1.setId(1L); - point1.setIdentifier("temperature"); - point1.setName("温度"); - point1.setFunctionCode(3); // FC03 读保持寄存器 - point1.setRegisterAddress(0); - point1.setRegisterCount(1); - point1.setRawDataType("INT16"); - point1.setByteOrder("BIG_ENDIAN"); - point1.setScale(new BigDecimal("0.1")); - point1.setPollInterval(5000); // 5 秒轮询一次 - points.add(point1); - - // 点位 2:湿度 - 保持寄存器 FC03, 地址 1, 1 个寄存器, UINT16 - IotModbusPointRespDTO point2 = new IotModbusPointRespDTO(); - point2.setId(2L); - point2.setIdentifier("humidity"); - point2.setName("湿度"); - point2.setFunctionCode(3); // FC03 读保持寄存器 - point2.setRegisterAddress(1); - point2.setRegisterCount(1); - point2.setRawDataType("UINT16"); - point2.setByteOrder("BIG_ENDIAN"); - point2.setScale(new BigDecimal("0.1")); - point2.setPollInterval(5000); // 5 秒轮询一次 - points.add(point2); - - config.setPoints(points); - log.info("[buildMockConfigs][已加载 Mock 配置, deviceId={}, points={}]", config.getDeviceId(), points.size()); - return Collections.singletonList(config); - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java index a02601d666..76e7d11e94 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java @@ -78,7 +78,7 @@ public class IotModbusTcpSlavePendingRequestManager { return matchByTransactionId(queue, frame.getTransactionId()); } // RTU 模式:FIFO,匹配 slaveId + functionCode + registerCount - int responseRegisterCount = extractRegisterCountFromResponse(frame); + int responseRegisterCount = IotModbusCommonUtils.extractRegisterCountFromResponse(frame); return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode(), responseRegisterCount); } @@ -115,29 +115,6 @@ public class IotModbusTcpSlavePendingRequestManager { return null; } - // TODO @AI:是不是放到 modbus 工具类里,更合适? - /** - * 从响应帧中提取 registerCount(通过 PDU 的 byteCount 推断) - * - * @return registerCount,无法提取时返回 -1(匹配时跳过校验) - */ - private int extractRegisterCountFromResponse(IotModbusFrame frame) { - byte[] pdu = frame.getPdu(); - if (pdu == null || pdu.length < 1) { - return -1; - } - int byteCount = pdu[0] & 0xFF; - int fc = frame.getFunctionCode(); - // FC03/04 寄存器读响应:registerCount = byteCount / 2 - if (fc == IotModbusCommonUtils.FC_READ_HOLDING_REGISTERS - || fc == IotModbusCommonUtils.FC_READ_INPUT_REGISTERS) { - return byteCount / 2; - } - // FC01/02 线圈/离散输入读响应:registerCount = byteCount * 8(线圈数量) - // 但因为按 bit 打包有余位,无法精确反推,返回 -1 跳过校验 - return -1; - } - /** * 清理过期请求 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java index 00a0c4b849..6b452e4b7e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; import io.vertx.mqtt.MqttEndpoint; import lombok.extern.slf4j.Slf4j; @@ -58,6 +59,11 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { Assert.notNull(connectionInfo, "无法获取连接信息"); Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配"); Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配"); + // 1.4 校验 topic 是否允许发布 + if (!IotMqttTopicUtils.isTopicPublishAllowed(topic, productKey, deviceName)) { + log.warn("[handleBusinessRequest][topic 不允许发布,客户端 ID: {},主题: {}]", clientId, topic); + return; + } // 2. 反序列化消息 message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java index 7b248ab7c9..4c83a4a256 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java +++ b/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 { this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); } - // TODO done @AI:vertx 有 udp 的实现么?当前已使用 Vert.x DatagramSocket 实现 /** * 处理 UDP 数据包 * diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java index 249b31544f..e72597d8fe 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java @@ -101,8 +101,6 @@ public final class IotMqttTopicUtils { * @param deviceName 设备名称 * @return 是否允许发布 */ - // TODO DONE @AI:这个逻辑,是不是 mqtt 协议,也要使用???答:是通用工具方法,MQTT 协议可按需调用; - // TODO @AI:那你改下 mqtt,也调用!!! public static boolean isTopicPublishAllowed(String topic, String productKey, String deviceName) { if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { return false; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/ModbusTcpSlaveSimulatorTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IoTModbusTcpMasterIntegrationTest.java similarity index 53% rename from yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/ModbusTcpSlaveSimulatorTest.java rename to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IoTModbusTcpMasterIntegrationTest.java index f901bc3973..40c8c5cdeb 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/ModbusTcpSlaveSimulatorTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IoTModbusTcpMasterIntegrationTest.java @@ -3,21 +3,49 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; import com.ghgande.j2mod.modbus.procimg.*; import com.ghgande.j2mod.modbus.slave.ModbusSlave; import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; /** - * Modbus TCP 从站模拟器 + * Modbus TCP 从站模拟器(手动测试) * - * 用于测试 Modbus TCP 网关的连接和数据读写功能 + *

                              测试场景:模拟一个标准 Modbus TCP 从站设备,供 Modbus TCP Master 网关连接和读写数据 + * + *

                              使用步骤: + *

                                + *
                              1. 运行 {@link #testStartSlaveSimulator()} 启动模拟从站(默认端口 5020,从站地址 1)
                              2. + *
                              3. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-master 协议)
                              4. + *
                              5. 确保数据库有对应的 Modbus Master 设备配置(ip=127.0.0.1, port=5020, slaveId=1)
                              6. + *
                              7. 网关会自动连接模拟从站并开始轮询读取寄存器数据
                              8. + *
                              9. 模拟器每 5 秒自动更新输入寄存器和保持寄存器的值,模拟传感器数据变化
                              10. + *
                              + * + *

                              可用寄存器: + *

                                + *
                              • 线圈 (Coil, 功能码 01/05): 地址 0-9,交替 true/false
                              • + *
                              • 离散输入 (Discrete Input, 功能码 02): 地址 0-9,每 3 个一个 true
                              • + *
                              • 保持寄存器 (Holding Register, 功能码 03/06/16): 地址 0-19,初始值 0,100,200,...
                              • + *
                              • 输入寄存器 (Input Register, 功能码 04): 地址 0-19,初始值 1,11,21,...
                              • + *
                              * * @author 芋道源码 */ -public class ModbusTcpSlaveSimulatorTest { +@Slf4j +@Disabled +public class IoTModbusTcpMasterIntegrationTest { private static final int PORT = 5020; private static final int SLAVE_ID = 1; - @SuppressWarnings({"InfiniteLoopStatement", "BusyWait", "CommentedOutCode"}) - public static void main(String[] args) throws Exception { + /** + * 启动 Modbus TCP 从站模拟器 + * + *

                              模拟器会持续运行,每 5 秒更新一次寄存器数据,直到手动停止 + */ + @SuppressWarnings({"InfiniteLoopStatement", "BusyWait"}) + @Test + public void testStartSlaveSimulator() throws Exception { // 1. 创建进程映像(Process Image),存储寄存器数据 SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID); @@ -53,24 +81,15 @@ public class ModbusTcpSlaveSimulatorTest { // 3.2 启动从站 slave.open(); - System.out.println("==================================================="); - System.out.println("Modbus TCP 从站模拟器已启动"); - System.out.println("端口: " + PORT); - System.out.println("从站地址 (Slave ID): " + SLAVE_ID); - System.out.println("==================================================="); - System.out.println("可用寄存器:"); - System.out.println(" - 线圈 (Coil, 功能码 01/05): 地址 0-9"); - System.out.println(" - 离散输入 (Discrete Input, 功能码 02): 地址 0-9"); - System.out.println(" - 保持寄存器 (Holding Register, 功能码 03/06/16): 地址 0-19"); - System.out.println(" - 输入寄存器 (Input Register, 功能码 04): 地址 0-19"); - System.out.println("==================================================="); - System.out.println("按 Ctrl+C 停止模拟器"); + log.info("[testStartSlaveSimulator][Modbus TCP 从站模拟器已启动, 端口: {}, 从站地址: {}]", PORT, SLAVE_ID); + log.info("[testStartSlaveSimulator][可用寄存器: 线圈(01/05) 0-9, 离散输入(02) 0-9, " + + "保持寄存器(03/06/16) 0-19, 输入寄存器(04) 0-19]"); // 4. 添加关闭钩子 Runtime.getRuntime().addShutdownHook(new Thread(() -> { - System.out.println("\n正在关闭模拟器..."); + log.info("[testStartSlaveSimulator][正在关闭模拟器...]"); slave.close(); - System.out.println("模拟器已关闭"); + log.info("[testStartSlaveSimulator][模拟器已关闭]"); })); // 5. 保持运行,定时更新输入寄存器模拟数据变化 @@ -87,9 +106,8 @@ public class ModbusTcpSlaveSimulatorTest { // 更新保持寄存器的第一个值 spi.getRegister(0).setValue(counter * 100); -// System.out.println("[" + java.time.LocalTime.now() + "] 数据已更新, counter=" + counter -// + ", 保持寄存器[0]=" + (counter * 100) -// + ", 输入寄存器[0]=" + (1 + counter)); + log.info("[testStartSlaveSimulator][数据已更新, counter={}, 保持寄存器[0]={}, 输入寄存器[0]={}]", + counter, counter * 100, 1 + counter); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java similarity index 93% rename from yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java rename to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java index e103d6b9d3..286923377a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java @@ -1,14 +1,15 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; +import cn.hutool.core.util.HexUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetClient; @@ -24,6 +25,8 @@ import org.junit.jupiter.api.Test; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import static org.junit.jupiter.api.Assertions.assertEquals; + /** * IoT Modbus TCP Slave 协议集成测试 — MODBUS_RTU 帧格式(手动测试) * @@ -46,7 +49,7 @@ import java.util.concurrent.TimeUnit; */ @Slf4j @Disabled -public class IotModbusTcpSlaveModbusRtuIntegrationTest { +public class IotModbusTcpSlaveRtuIntegrationTest { private static final String SERVER_HOST = "127.0.0.1"; private static final int SERVER_PORT = 503; @@ -65,9 +68,9 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; - private static final String DEVICE_NAME = "small"; - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; + private static final String PRODUCT_KEY = "modbus_tcp_slave_product_demo"; + private static final String DEVICE_NAME = "modbus_tcp_slave_device_demo_rtu"; + private static final String DEVICE_SECRET = "af01c55eb8e3424bb23fc6c783936b2e"; @BeforeAll static void setUp() { @@ -104,6 +107,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]", response.getSlaveId(), response.getFunctionCode(), response.getCustomData()); JSONObject json = JSONUtil.parseObj(response.getCustomData()); + assertEquals(0, json.getInt("code")); log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message")); } finally { socket.close(); @@ -122,10 +126,13 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { // 1. 先认证 IotModbusFrame authResponse = authenticate(socket); log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); + JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData()); + assertEquals(0, authJson.getInt("code")); // 2. 设置持续监听:每收到一个读请求,自动回复 log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); CompletableFuture done = new CompletableFuture<>(); + // 注意:使用 requestMode=true,因为设备端收到的是网关下发的读请求(非响应) RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> { log.info("[testPollingResponse][收到请求: slaveId={}, FC={}]", frame.getSlaveId(), frame.getFunctionCode()); @@ -144,7 +151,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { frame.getFunctionCode(), registerValues); socket.write(Buffer.buffer(responseData)); log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); - }); + }, true); socket.handler(parser); // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求 @@ -174,7 +181,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { IotModbusFrame writeRequest = waitForRequest(socket); log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, pdu={}]", writeRequest.getSlaveId(), writeRequest.getFunctionCode(), - bytesToHex(writeRequest.getPdu())); + HexUtil.encodeHexStr(writeRequest.getPdu())); } finally { socket.close(); } @@ -198,6 +205,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { */ private IotModbusFrame authenticate(NetSocket socket) throws Exception { IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + authInfo.setClientId(""); byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); return sendAndReceive(socket, authFrame); } @@ -291,18 +299,4 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { return frame; } - /** - * 字节数组转十六进制字符串 - */ - private static String bytesToHex(byte[] bytes) { - if (bytes == null) { - return "null"; - } - StringBuilder sb = new StringBuilder(); - for (byte b : bytes) { - sb.append(String.format("%02X ", b)); - } - return sb.toString().trim(); - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java similarity index 95% rename from yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java rename to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java index b8c1edb8a0..720887a914 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; +import cn.hutool.core.util.HexUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; @@ -25,7 +26,8 @@ import java.nio.ByteOrder; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -// TODO @芋艿:【晚点改】单测需要简化! +import static org.junit.jupiter.api.Assertions.assertEquals; + /** * IoT Modbus TCP Slave 协议集成测试 — MODBUS_TCP 帧格式(手动测试) * @@ -48,7 +50,7 @@ import java.util.concurrent.TimeUnit; */ @Slf4j @Disabled -public class IotModbusTcpSlaveModbusTcpIntegrationTest { +public class IotModbusTcpSlaveTcpIntegrationTest { private static final String SERVER_HOST = "127.0.0.1"; private static final int SERVER_PORT = 503; @@ -106,6 +108,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]", response.getSlaveId(), response.getFunctionCode(), response.getCustomData()); JSONObject json = JSONUtil.parseObj(response.getCustomData()); + assertEquals(0, json.getInt("code")); log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message")); } finally { socket.close(); @@ -124,7 +127,8 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { // 1. 先认证 IotModbusFrame authResponse = authenticate(socket); log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); - // TODO @AI:这里断言下,认证必须成功! + JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData()); + assertEquals(0, authJson.getInt("code")); // 2. 设置持续监听:每收到一个读请求,自动回复 log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); @@ -176,7 +180,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { IotModbusFrame writeRequest = waitForRequest(socket); log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, transactionId={}, pdu={}]", writeRequest.getSlaveId(), writeRequest.getFunctionCode(), - writeRequest.getTransactionId(), bytesToHex(writeRequest.getPdu())); + writeRequest.getTransactionId(), HexUtil.encodeHexStr(writeRequest.getPdu())); } finally { socket.close(); } @@ -295,18 +299,4 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { return buf.array(); } - /** - * 字节数组转十六进制字符串 - */ - private static String bytesToHex(byte[] bytes) { - if (bytes == null) { - return "null"; - } - StringBuilder sb = new StringBuilder(); - for (byte b : bytes) { - sb.append(String.format("%02X ", b)); - } - return sb.toString().trim(); - } - } From fcca74ac7d1335b805a07f191cefa43445306eda Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 9 Feb 2026 08:02:41 +0800 Subject: [PATCH 41/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=BC=98=E5=8C=96=E7=BD=91=E5=85=B3=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E7=9A=84=E8=B5=84=E6=BA=90=E6=B8=85=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E4=B8=BB=E8=A6=81=E6=98=AF=20stop0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IotDeviceModbusConfigServiceImpl.java | 1 - .../protocol/coap/IotCoapProtocol.java | 32 +++++++------ .../protocol/emqx/IotEmqxProtocol.java | 23 ++++----- .../protocol/http/IotHttpProtocol.java | 22 ++++----- .../common/utils/IotModbusCommonUtils.java | 5 +- .../tcpmaster/IotModbusTcpMasterProtocol.java | 36 +++++++------- .../IotModbusTcpMasterConnectionManager.java | 17 +++++-- .../tcpslave/IotModbusTcpSlaveProtocol.java | 47 +++++++------------ .../IotModbusTcpSlaveConnectionManager.java | 11 +++-- .../IotModbusTcpSlavePollScheduler.java | 2 + .../protocol/mqtt/IotMqttProtocol.java | 43 +++++++++-------- .../manager/IotMqttConnectionManager.java | 20 ++++++++ .../gateway/protocol/tcp/IotTcpProtocol.java | 43 +++++++++-------- .../tcp/manager/IotTcpConnectionManager.java | 20 ++++++++ .../gateway/protocol/udp/IotUdpProtocol.java | 38 +++++++-------- .../websocket/IotWebSocketProtocol.java | 45 +++++++++--------- .../IotWebSocketConnectionManager.java | 20 ++++++++ .../src/main/resources/application.yaml | 4 +- 18 files changed, 244 insertions(+), 185 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java index 265d9770f5..a97bcb14d2 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java @@ -70,7 +70,6 @@ public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigSe return modbusConfigMapper.selectList(listReqDTO); } - // TODO @AI:还是希望在 IotDeviceModbusConfigSaveReqVO 里,通过 validator 校验?!或者通过 group 来处理? private void validateModbusConfigByProtocolType(IotDeviceModbusConfigSaveReqVO saveReqVO, String protocolType) { IotProtocolTypeEnum protocolTypeEnum = IotProtocolTypeEnum.of(protocolType); if (protocolTypeEnum == null) { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java index 14fe10dcd8..4bc8cdbe28 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java +++ b/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 { /** * 下行消息订阅者 */ - private final IotCoapDownstreamSubscriber downstreamSubscriber; + private IotCoapDownstreamSubscriber downstreamSubscriber; public IotCoapProtocol(ProtocolProperties properties) { IotCoapConfig coapConfig = properties.getCoap(); Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); - - // 初始化下行消息订阅者 - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus); } @Override @@ -94,9 +90,9 @@ public class IotCoapProtocol implements IotProtocol { return; } - IotCoapConfig coapConfig = properties.getCoap(); try { // 1.1 创建 CoAP 配置 + IotCoapConfig coapConfig = properties.getCoap(); Configuration config = Configuration.createStandardWithoutFile(); config.set(CoapConfig.COAP_PORT, properties.getPort()); config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize()); @@ -131,13 +127,12 @@ public class IotCoapProtocol implements IotProtocol { getId(), properties.getPort(), serverId); // 4. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus); this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e); - if (coapServer != null) { - coapServer.destroy(); - coapServer = null; - } + stop0(); throw e; } } @@ -147,12 +142,19 @@ public class IotCoapProtocol implements IotProtocol { if (!running) { return; } + stop0(); + } + + private void stop0() { // 1. 停止下行消息订阅者 - try { - downstreamSubscriber.stop(); - log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId()); - } catch (Exception e) { - log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e); + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; } // 2. 关闭 CoAP 服务器 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java index a9e251736f..f110f64b4d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java +++ b/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 { /** * 下行消息订阅者 */ - private final IotEmqxDownstreamSubscriber downstreamSubscriber; + private IotEmqxDownstreamSubscriber downstreamSubscriber; public IotEmqxProtocol(ProtocolProperties properties) { Assert.notNull(properties, "协议实例配置不能为空"); @@ -101,10 +101,6 @@ public class IotEmqxProtocol implements IotProtocol { "MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空"); this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); this.upstreamHandler = new IotEmqxUpstreamHandler(serverId); - - // 初始化下行消息订阅者 - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus); } @Override @@ -124,7 +120,7 @@ public class IotEmqxProtocol implements IotProtocol { return; } - // 1.1 创建 Vertx 实例 + // 1.1 创建 Vertx 实例 和 下行消息订阅者 this.vertx = Vertx.vertx(); try { @@ -138,6 +134,8 @@ public class IotEmqxProtocol implements IotProtocol { getId(), properties.getPort(), serverId); // 2. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus); this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e); @@ -157,11 +155,14 @@ public class IotEmqxProtocol implements IotProtocol { private void stop0() { // 1. 停止下行消息订阅者 - try { - downstreamSubscriber.stop(); - log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId()); - } catch (Exception e) { - log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e); + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; } // 2.1 先置为 false:避免 closeHandler 触发重连 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java index 2f92419161..f6c9bdc900 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java +++ b/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 { public IotHttpProtocol(ProtocolProperties properties) { this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); - - // 初始化下行消息订阅者 - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); } @Override @@ -87,7 +83,7 @@ public class IotHttpProtocol implements IotProtocol { return; } - // 1.1 创建 Vertx 实例(每个 Protocol 独立管理) + // 1.1 创建 Vertx 实例 this.vertx = Vertx.vertx(); // 1.2 创建路由 @@ -123,18 +119,12 @@ public class IotHttpProtocol implements IotProtocol { getId(), properties.getPort(), serverId); // 2. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); - // 启动失败时关闭资源 - if (httpServer != null) { - httpServer.close(); - httpServer = null; - } - if (vertx != null) { - vertx.close(); - vertx = null; - } + stop0(); throw e; } } @@ -144,6 +134,10 @@ public class IotHttpProtocol implements IotProtocol { if (!running) { return; } + stop0(); + } + + private void stop0() { // 1. 停止下行消息订阅者 if (downstreamSubscriber != null) { try { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java index 32e7292964..28889c321a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java @@ -381,8 +381,11 @@ public class IotModbusCommonUtils { case UINT16: return new int[]{rawValue.intValue() & 0xFFFF}; case INT32: - case UINT32: return encodeInt32(rawValue.intValue(), byteOrder); + case UINT32: + // 使用 longValue() 避免超过 Integer.MAX_VALUE 时溢出, + // 强转 int 保留低 32 位 bit pattern,写入寄存器的字节是正确的无符号值 + return encodeInt32((int) rawValue.longValue(), byteOrder); case FLOAT: return encodeFloat(rawValue.floatValue(), byteOrder); case DOUBLE: diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java index 1b284e4e4c..31271b6350 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java @@ -65,7 +65,7 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { /** * 下行消息订阅者 */ - private final IotModbusTcpMasterDownstreamSubscriber downstreamSubscriber; + private IotModbusTcpMasterDownstreamSubscriber downstreamSubscriber; private final IotModbusTcpMasterConfigCacheService configCacheService; private final IotModbusTcpMasterPollScheduler pollScheduler; @@ -89,15 +89,9 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { // 初始化 Handler IotModbusTcpMasterUpstreamHandler upstreamHandler = new IotModbusTcpMasterUpstreamHandler(messageService, serverId); - IotModbusTcpMasterDownstreamHandler downstreamHandler = new IotModbusTcpMasterDownstreamHandler(connectionManager, - configCacheService); // 初始化轮询调度器 this.pollScheduler = new IotModbusTcpMasterPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService); - - // 初始化下行消息订阅者 - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - this.downstreamSubscriber = new IotModbusTcpMasterDownstreamSubscriber(this, downstreamHandler, messageBus); } @Override @@ -130,13 +124,14 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { log.info("[start][IoT Modbus TCP Master 协议 {} 启动成功,serverId={}]", getId(), serverId); // 2. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotModbusTcpMasterDownstreamHandler downstreamHandler = new IotModbusTcpMasterDownstreamHandler(connectionManager, + configCacheService); + this.downstreamSubscriber = new IotModbusTcpMasterDownstreamSubscriber(this, downstreamHandler, messageBus); this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT Modbus TCP Master 协议 {} 启动失败]", getId(), e); - // 启动失败时关闭资源 - if (vertx != null) { - vertx.close(); - } + stop0(); throw e; } } @@ -146,12 +141,19 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { if (!running) { return; } + stop0(); + } + + private void stop0() { // 1. 停止下行消息订阅者 - try { - downstreamSubscriber.stop(); - log.info("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者已停止]", getId()); - } catch (Exception e) { - log.error("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者停止失败]", getId(), e); + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; } // 2.1 取消配置刷新定时器 @@ -161,10 +163,8 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { } // 2.2 停止轮询调度器 pollScheduler.stopAll(); - log.info("[stop][IoT Modbus TCP Master 协议 {} 轮询调度器已停止]", getId()); // 2.3 关闭所有连接 connectionManager.closeAll(); - log.info("[stop][IoT Modbus TCP Master 协议 {} 连接管理器已关闭]", getId()); // 3. 关闭 Vert.x 实例 if (vertx != null) { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java index ebbca8e2a0..4fb605a932 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java @@ -14,6 +14,8 @@ import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -185,8 +187,11 @@ public class IotModbusTcpMasterConnectionManager { */ private void addDeviceAndOnline(ModbusConnection connection, IotModbusDeviceConfigRespDTO config) { - connection.addDevice(config.getDeviceId(), config.getSlaveId()); - sendOnlineMessage(config); + Integer previous = connection.addDevice(config.getDeviceId(), config.getSlaveId()); + // 首次注册,发送上线消息 + if (previous == null) { + sendOnlineMessage(config); + } } /** @@ -247,7 +252,9 @@ public class IotModbusTcpMasterConnectionManager { * 关闭所有连接 */ public void closeAll() { - for (String connectionKey : connectionPool.keySet()) { + // 先复制再遍历,避免 closeConnection 中 remove 导致并发修改 + List connectionKeys = new ArrayList<>(connectionPool.keySet()); + for (String connectionKey : connectionKeys) { closeConnection(connectionKey); } deviceConnectionMap.clear(); @@ -282,8 +289,8 @@ public class IotModbusTcpMasterConnectionManager { */ private Context context; - public void addDevice(Long deviceId, Integer slaveId) { - deviceSlaveMap.put(deviceId, slaveId); + public Integer addDevice(Long deviceId, Integer slaveId) { + return deviceSlaveMap.putIfAbsent(deviceId, slaveId); } public void removeDevice(Long deviceId) { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java index 3a44a189fe..feb89811db 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java @@ -88,7 +88,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { /** * 下行消息订阅者 */ - private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; + private IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; private final IotModbusFrameDecoder frameDecoder; @SuppressWarnings("FieldCanBeLocal") @@ -121,7 +121,6 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { // 初始化共享事务 ID 自增器(PollScheduler 和 DownstreamHandler 共用,避免 transactionId 冲突) AtomicInteger transactionIdCounter = new AtomicInteger(0); - // 初始化轮询调度器 this.pollScheduler = new IotModbusTcpSlavePollScheduler( vertx, connectionManager, frameEncoder, pendingRequestManager, @@ -134,13 +133,6 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { deviceApi, this.messageService, frameEncoder, connectionManager, configCacheService, pendingRequestManager, pollScheduler, deviceService, serverId); - - // 初始化下行消息订阅者 - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - IotModbusTcpSlaveDownstreamHandler downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( - connectionManager, configCacheService, frameEncoder, transactionIdCounter); - this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber( - this, downstreamHandler, messageBus); } @Override @@ -179,25 +171,15 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { getId(), serverId, properties.getPort()); // 3. 启动下行消息订阅 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotModbusTcpSlaveDownstreamHandler downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( + connectionManager, configCacheService, frameEncoder, this.pollScheduler.getTransactionIdCounter()); + this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber( + this, downstreamHandler, messageBus); downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT Modbus TCP Slave 协议 {} 启动失败]", getId(), e); - // TODO @芋艿:后续统一优化 stop 逻辑; - if (configRefreshTimerId != null) { - vertx.cancelTimer(configRefreshTimerId); - configRefreshTimerId = null; - } - if (requestCleanupTimerId != null) { - vertx.cancelTimer(requestCleanupTimerId); - requestCleanupTimerId = null; - } - connectionManager.closeAll(); - if (netServer != null) { - netServer.close(); - } - if (vertx != null) { - vertx.close(); - } + stop0(); throw e; } } @@ -207,12 +189,18 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { if (!running) { return; } + stop0(); + } + private void stop0() { // 1. 停止下行消息订阅 - try { - downstreamSubscriber.stop(); - } catch (Exception e) { - log.error("[stop][下行消息订阅器停止失败]", e); + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + } catch (Exception e) { + log.error("[stop][下行消息订阅器停止失败]", e); + } + downstreamSubscriber = null; } // 2.1 取消定时器 @@ -238,6 +226,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { } catch (Exception e) { log.error("[stop][TCP Server 关闭失败]", e); } + netServer = null; } // 3. 关闭 Vertx diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java index 04434910aa..16899c08fe 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java @@ -8,6 +8,8 @@ import lombok.Data; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -155,15 +157,18 @@ public class IotModbusTcpSlaveConnectionManager { * 关闭所有连接 */ public void closeAll() { - for (NetSocket socket : connectionMap.keySet()) { + // 1. 先复制再清空,避免 closeHandler 回调时并发修改 + List sockets = new ArrayList<>(connectionMap.keySet()); + connectionMap.clear(); + deviceSocketMap.clear(); + // 2. 关闭所有 socket(closeHandler 中 removeConnection 发现 map 为空会安全跳过) + for (NetSocket socket : sockets) { try { socket.close(); } catch (Exception e) { log.error("[closeAll][关闭连接失败]", e); } } - connectionMap.clear(); - deviceSocketMap.clear(); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java index f1276eaa52..80bef684db 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotMod import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; import io.vertx.core.Vertx; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.atomic.AtomicInteger; @@ -31,6 +32,7 @@ public class IotModbusTcpSlavePollScheduler extends AbstractIotModbusPollSchedul /** * TCP 事务 ID 自增器(与 DownstreamHandler 共享) */ + @Getter private final AtomicInteger transactionIdCounter; public IotModbusTcpSlavePollScheduler(Vertx vertx, diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java index 1201fd1a42..354cd1b452 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java +++ b/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 { /** * 下行消息订阅者 */ - private final IotMqttDownstreamSubscriber downstreamSubscriber; + private IotMqttDownstreamSubscriber downstreamSubscriber; private final IotDeviceMessageService deviceMessageService; @@ -104,11 +104,6 @@ public class IotMqttProtocol implements IotProtocol { this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId); this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService); this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId); - - // 初始化下行消息订阅者 - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager); - this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus); } @Override @@ -157,18 +152,13 @@ public class IotMqttProtocol implements IotProtocol { getId(), properties.getPort(), serverId); // 2. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager); + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus); this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e); - // 启动失败时关闭资源 - if (mqttServer != null) { - mqttServer.close(); - mqttServer = null; - } - if (vertx != null) { - vertx.close(); - vertx = null; - } + stop0(); throw e; } } @@ -178,15 +168,24 @@ public class IotMqttProtocol implements IotProtocol { if (!running) { return; } + stop0(); + } + + private void stop0() { // 1. 停止下行消息订阅者 - try { - downstreamSubscriber.stop(); - log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId()); - } catch (Exception e) { - log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e); + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; } - // 2.1 关闭 MQTT 服务器 + // 2.1 关闭所有连接 + connectionManager.closeAll(); + // 2.2 关闭 MQTT 服务器 if (mqttServer != null) { try { mqttServer.close().result(); @@ -196,7 +195,7 @@ public class IotMqttProtocol implements IotProtocol { } mqttServer = null; } - // 2.2 关闭 Vertx 实例 + // 2.3 关闭 Vertx 实例 if (vertx != null) { try { vertx.close().result(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java index ccb9fa5a60..9bd3ec4934 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java +++ b/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; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -166,6 +168,24 @@ public class IotMqttConnectionManager { return deviceEndpointMap.get(deviceId); } + /** + * 关闭所有连接 + */ + public void closeAll() { + // 1. 先复制再清空,避免 closeHandler 回调时并发修改 + List endpoints = new ArrayList<>(connectionMap.keySet()); + connectionMap.clear(); + deviceEndpointMap.clear(); + // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过) + for (MqttEndpoint endpoint : endpoints) { + try { + endpoint.close(); + } catch (Exception ignored) { + // 连接可能已关闭,忽略异常 + } + } + } + /** * 连接信息 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java index e864df543e..8660e87f7e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java +++ b/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 { /** * 下行消息订阅者 */ - private final IotTcpDownstreamSubscriber downstreamSubscriber; + private IotTcpDownstreamSubscriber downstreamSubscriber; /** * 消息序列化器 @@ -94,11 +94,6 @@ public class IotTcpProtocol implements IotProtocol { // 初始化连接管理器 this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections()); - - // 初始化下行消息订阅者 - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); - this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus); } @Override @@ -152,18 +147,13 @@ public class IotTcpProtocol implements IotProtocol { getId(), properties.getPort(), serverId); // 2. 启动下行消息订阅者 + IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus); this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT TCP 协议 {} 启动失败]", getId(), e); - // 启动失败时关闭资源 - if (tcpServer != null) { - tcpServer.close(); - tcpServer = null; - } - if (vertx != null) { - vertx.close(); - vertx = null; - } + stop0(); throw e; } } @@ -173,15 +163,24 @@ public class IotTcpProtocol implements IotProtocol { if (!running) { return; } + stop0(); + } + + private void stop0() { // 1. 停止下行消息订阅者 - try { - downstreamSubscriber.stop(); - log.info("[stop][IoT TCP 协议 {} 下行消息订阅者已停止]", getId()); - } catch (Exception e) { - log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e); + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT TCP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; } - // 2.1 关闭 TCP 服务器 + // 2.1 关闭所有连接 + connectionManager.closeAll(); + // 2.2 关闭 TCP 服务器 if (tcpServer != null) { try { tcpServer.close().result(); @@ -191,7 +190,7 @@ public class IotTcpProtocol implements IotProtocol { } tcpServer = null; } - // 2.2 关闭 Vertx 实例 + // 2.3 关闭 Vertx 实例 if (vertx != null) { try { vertx.close().result(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java index b7b72a370b..20065f1b40 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java +++ b/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; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -122,6 +124,24 @@ public class IotTcpConnectionManager { } } + /** + * 关闭所有连接 + */ + public void closeAll() { + // 1. 先复制再清空,避免 closeHandler 回调时并发修改 + List sockets = new ArrayList<>(connectionMap.keySet()); + connectionMap.clear(); + deviceSocketMap.clear(); + // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过) + for (NetSocket socket : sockets) { + try { + socket.close(); + } catch (Exception ignored) { + // 连接可能已关闭,忽略异常 + } + } + } + /** * 连接信息(包含认证信息) */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java index bfed2d9c32..9df3c8bce7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java +++ b/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 { /** * 下行消息订阅者 */ - private final IotUdpDownstreamSubscriber downstreamSubscriber; + private IotUdpDownstreamSubscriber downstreamSubscriber; /** * 消息序列化器 @@ -85,10 +85,6 @@ public class IotUdpProtocol implements IotProtocol { // 初始化会话管理器 this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions(), udpConfig.getSessionTimeoutMs()); - // 初始化下行消息订阅者 - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - IotUdpDownstreamHandler downstreamHandler = new IotUdpDownstreamHandler(this, sessionManager, serializer); - this.downstreamSubscriber = new IotUdpDownstreamSubscriber(this, downstreamHandler, messageBus); } @Override @@ -108,8 +104,11 @@ public class IotUdpProtocol implements IotProtocol { return; } - // 1.1 创建 Vertx 实例 + // 1.1 创建 Vertx 实例 和 下行消息订阅者 this.vertx = Vertx.vertx(); + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotUdpDownstreamHandler downstreamHandler = new IotUdpDownstreamHandler(this, sessionManager, serializer); + this.downstreamSubscriber = new IotUdpDownstreamSubscriber(this, downstreamHandler, messageBus); // 1.2 创建 UDP Socket 选项 IotUdpConfig udpConfig = properties.getUdp(); @@ -137,15 +136,7 @@ public class IotUdpProtocol implements IotProtocol { this.downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT UDP 协议 {} 启动失败]", getId(), e); - // 启动失败时关闭资源 - if (udpSocket != null) { - udpSocket.close(); - udpSocket = null; - } - if (vertx != null) { - vertx.close(); - vertx = null; - } + stop0(); throw e; } } @@ -155,12 +146,19 @@ public class IotUdpProtocol implements IotProtocol { if (!running) { return; } + stop0(); + } + + private void stop0() { // 1. 停止下行消息订阅者 - try { - downstreamSubscriber.stop(); - log.info("[stop][IoT UDP 协议 {} 下行消息订阅者已停止]", getId()); - } catch (Exception e) { - log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e); + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT UDP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; } // 2.1 关闭 UDP Socket diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java index b416900db5..083dc32369 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; @@ -9,8 +10,8 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.upstream.IotWebSocketUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; @@ -21,7 +22,6 @@ import io.vertx.core.http.HttpServerOptions; import io.vertx.core.net.PemKeyCertOptions; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import cn.hutool.core.lang.Assert; /** * IoT WebSocket 协议实现 @@ -65,7 +65,7 @@ public class IotWebSocketProtocol implements IotProtocol { /** * 下行消息订阅者 */ - private final IotWebSocketDownstreamSubscriber downstreamSubscriber; + private IotWebSocketDownstreamSubscriber downstreamSubscriber; /** * 消息序列化器 @@ -87,10 +87,6 @@ public class IotWebSocketProtocol implements IotProtocol { // 初始化连接管理器 this.connectionManager = new IotWebSocketConnectionManager(); - // 初始化下行消息订阅者 - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - IotWebSocketDownstreamHandler downstreamHandler = new IotWebSocketDownstreamHandler(serializer, connectionManager); - this.downstreamSubscriber = new IotWebSocketDownstreamSubscriber(this, downstreamHandler, messageBus); } @Override @@ -152,17 +148,13 @@ public class IotWebSocketProtocol implements IotProtocol { getId(), properties.getPort(), wsConfig.getPath(), serverId); // 2. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotWebSocketDownstreamHandler downstreamHandler = new IotWebSocketDownstreamHandler(serializer, connectionManager); + this.downstreamSubscriber = new IotWebSocketDownstreamSubscriber(this, downstreamHandler, messageBus); downstreamSubscriber.start(); } catch (Exception e) { log.error("[start][IoT WebSocket 协议 {} 启动失败]", getId(), e); - if (httpServer != null) { - httpServer.close(); - httpServer = null; - } - if (vertx != null) { - vertx.close(); - vertx = null; - } + stop0(); throw e; } } @@ -172,15 +164,24 @@ public class IotWebSocketProtocol implements IotProtocol { if (!running) { return; } + stop0(); + } + + private void stop0() { // 1. 停止下行消息订阅者 - try { - downstreamSubscriber.stop(); - log.info("[stop][IoT WebSocket 协议 {} 下行消息订阅者已停止]", getId()); - } catch (Exception e) { - log.error("[stop][IoT WebSocket 协议 {} 下行消息订阅者停止失败]", getId(), e); + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT WebSocket 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT WebSocket 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; } - // 2.1 关闭 WebSocket 服务器 + // 2.1 关闭所有连接 + connectionManager.closeAll(); + // 2.2 关闭 WebSocket 服务器 if (httpServer != null) { try { httpServer.close().result(); @@ -190,7 +191,7 @@ public class IotWebSocketProtocol implements IotProtocol { } httpServer = null; } - // 2.2 关闭 Vertx 实例 + // 2.3 关闭 Vertx 实例 if (vertx != null) { try { vertx.close().result(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java index 8b09da0f98..92019ffadd 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java +++ b/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; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -114,6 +116,24 @@ public class IotWebSocketConnectionManager { } } + /** + * 关闭所有连接 + */ + public void closeAll() { + // 1. 先复制再清空,避免 closeHandler 回调时并发修改 + List sockets = new ArrayList<>(connectionMap.keySet()); + connectionMap.clear(); + deviceSocketMap.clear(); + // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过) + for (ServerWebSocket socket : sockets) { + try { + socket.close(); + } catch (Exception ignored) { + // 连接可能已关闭,忽略异常 + } + } + } + /** * 连接信息(包含认证信息) */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 1ccd9f37b3..f5c77dbea0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -167,7 +167,7 @@ yudao: # 针对引入的 Modbus TCP Master 组件的配置 # ==================================== - id: modbus-tcp-master-1 - enabled: true + enabled: false protocol: modbus_tcp_master port: 502 modbus-tcp-master: @@ -176,7 +176,7 @@ yudao: # 针对引入的 Modbus TCP Slave 组件的配置 # ==================================== - id: modbus-tcp-slave-1 - enabled: true + enabled: false protocol: modbus_tcp_slave port: 503 modbus-tcp-slave: From b88d9a702e31e93859136e599d7a9c1459b47b32 Mon Sep 17 00:00:00 2001 From: mcarthorlee Date: Mon, 9 Feb 2026 03:54:45 +0000 Subject: [PATCH 42/53] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dmaven=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E9=94=99=E8=AF=AF=EF=BC=9AAuthController.java=E4=B8=AD?= =?UTF-8?q?=E6=9C=AAimport=20DataPermission=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: mcarthorlee --- .../module/system/controller/admin/auth/AuthController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java index 6f95673c4b..974585af30 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java @@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; import cn.iocoder.yudao.framework.security.config.SecurityProperties; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; From b800d274a4ce2dfb0a5a401545c0d50005cdafe7 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 9 Feb 2026 13:10:08 +0800 Subject: [PATCH 43/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20MQTT=20=E8=AE=BE=E5=A4=87=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E6=B6=88=E6=81=AF=E7=9A=84=20=5Freply=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E6=A0=87=E5=87=86=E5=8C=96=EF=BC=8C=E5=B9=B6=E7=B2=BE?= =?UTF-8?q?=E7=AE=80=E4=B8=8A=E8=A1=8C=E5=A4=84=E7=90=86=E5=99=A8=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/api/device/IoTDeviceApiImpl.java | 3 ++ .../product/IotProductServiceImpl.java | 6 ++-- .../thingmodel/IotThingModelServiceImpl.java | 2 +- .../upstream/IotEmqxUpstreamHandler.java | 5 ++- .../upstream/IotMqttUpstreamHandler.java | 36 ++++--------------- .../iot/gateway/util/IotMqttTopicUtils.java | 28 +++++++++++++++ ...rectDeviceMqttProtocolIntegrationTest.java | 26 +++++++++++--- .../service/account/MpAccountServiceImpl.java | 2 +- 8 files changed, 68 insertions(+), 40 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java index 6bfe0e4e92..7922eb1b24 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java @@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; +import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; @@ -51,8 +52,10 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { @Resource private IotProductService productService; @Resource + @Lazy // 延迟加载,解决循环依赖 private IotDeviceModbusConfigService modbusConfigService; @Resource + @Lazy // 延迟加载,解决循环依赖 private IotDeviceModbusPointService modbusPointService; @Override diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java index 9686f42c93..49d7f36958 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java @@ -17,6 +17,7 @@ import com.baomidou.dynamic.datasource.annotation.DSTransactional; import jakarta.annotation.Resource; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -40,10 +41,11 @@ public class IotProductServiceImpl implements IotProductService { @Resource private IotProductMapper productMapper; - @Resource - private IotDevicePropertyService devicePropertyDataService; @Resource private IotDeviceService deviceService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private IotDevicePropertyService devicePropertyDataService; @Override public Long createProduct(IotProductSaveReqVO createReqVO) { diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java index 18a5b62e2a..4a8b97475b 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java @@ -51,8 +51,8 @@ public class IotThingModelServiceImpl implements IotThingModelService { @Resource @Lazy // 延迟加载,解决循环依赖 private IotProductService productService; - @Resource + @Lazy // 延迟加载,解决循环依赖 private IotDeviceModbusPointService deviceModbusPointService; @Override diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java index 17d5f85fc0..45c94818cb 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java +++ b/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; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; import io.vertx.mqtt.messages.MqttPublishMessage; import lombok.extern.slf4j.Slf4j; @@ -42,12 +43,14 @@ public class IotEmqxUpstreamHandler { return; } - // 2. 反序列化消息 + // 2.1 反序列化消息 IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); if (message == null) { log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); return; } + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀) + IotMqttTopicUtils.normalizeReplyMethod(message); // 3. 发送消息到队列 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java index 6b452e4b7e..81a6ece1a4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; -import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; @@ -10,8 +9,6 @@ import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; import io.vertx.mqtt.MqttEndpoint; import lombok.extern.slf4j.Slf4j; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; /** * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等) @@ -39,10 +36,6 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { */ public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) { String clientId = endpoint.clientIdentifier(); - IotDeviceMessage message = null; - String productKey = null; - String deviceName = null; - try { // 1.1 基础检查 if (ArrayUtil.isEmpty(payload)) { @@ -50,8 +43,8 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { } // 1.2 解析主题,获取 productKey 和 deviceName String[] topicParts = topic.split("/"); - productKey = ArrayUtil.get(topicParts, 2); - deviceName = ArrayUtil.get(topicParts, 3); + String productKey = ArrayUtil.get(topicParts, 2); + String deviceName = ArrayUtil.get(topicParts, 3); Assert.notBlank(productKey, "产品 Key 不能为空"); Assert.notBlank(deviceName, "设备名称不能为空"); // 1.3 校验设备信息,防止伪造设备消息 @@ -65,38 +58,21 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { return; } - // 2. 反序列化消息 - message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); + // 2.1 反序列化消息 + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); if (message == null) { log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); - sendErrorResponse(endpoint, productKey, deviceName, null, null, - BAD_REQUEST.getCode(), "消息解码失败"); return; } + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀) + IotMqttTopicUtils.normalizeReplyMethod(message); // 3. 处理业务消息 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic); - } catch (ServiceException e) { - log.warn("[handleBusinessRequest][业务异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage()); - String requestId = message != null ? message.getRequestId() : null; - String method = message != null ? message.getMethod() : null; - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage()); - } catch (IllegalArgumentException e) { - log.warn("[handleBusinessRequest][参数校验失败,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage()); - String requestId = message != null ? message.getRequestId() : null; - String method = message != null ? message.getMethod() : null; - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, - BAD_REQUEST.getCode(), e.getMessage()); } catch (Exception e) { log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]", clientId, topic, e.getMessage(), e); - String requestId = message != null ? message.getRequestId() : null; - String method = message != null ? message.getMethod() : null; - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, - INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java index e72597d8fe..60e8d6c7be 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.iot.gateway.util; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; /** * IoT 网关 MQTT 主题工具类 @@ -44,6 +46,32 @@ public final class IotMqttTopicUtils { */ public static final String MQTT_ACL_PATH = "/mqtt/acl"; + // ========== 消息方法标准化 ========== + + /** + * 标准化设备回复消息的 method + *

                              + * MQTT 协议中,设备回复下行指令时,topic 和 method 会携带 _reply 后缀 + * (如 thing.service.invoke_reply)。平台内部统一使用基础 method(如 thing.service.invoke), + * 通过 {@link IotDeviceMessage#getCode()} 非空来识别回复消息。 + *

                              + * 此方法剥离 _reply 后缀,并确保 code 字段被设置。 + * + * @param message 设备消息 + */ + public static void normalizeReplyMethod(IotDeviceMessage message) { + String method = message.getMethod(); + if (!StrUtil.endWith(method, REPLY_TOPIC_SUFFIX)) { + return; + } + // 1. 剥离 _reply 后缀 + message.setMethod(method.substring(0, method.length() - REPLY_TOPIC_SUFFIX.length())); + // 2. 确保 code 被设置,使 isReplyMessage() 能正确识别 + if (message.getCode() == null) { + message.setCode(GlobalErrorCodeConstants.SUCCESS.getCode()); + } + } + // ========== 工具方法 ========== /** diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java index 45cb7ca450..3333af6a71 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java +++ b/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 { log.info("[testSubscribe][连接认证成功]"); try { - // 2. 设置消息处理器 - client.publishHandler(message -> log.info("[testSubscribe][收到消息: topic={}, payload={}]", - message.topicName(), message.payload().toString())); + // 2. 设置消息处理器:收到属性设置时,回复 _reply 消息 + client.publishHandler(message -> { + log.info("[testSubscribe][收到消息: topic={}, payload={}]", + message.topicName(), message.payload().toString()); + // 收到属性设置消息时,回复 _reply + if (message.topicName().endsWith("/thing/property/set")) { + try { + IotDeviceMessage received = SERIALIZER.deserialize(message.payload().getBytes()); + IotDeviceMessage reply = IotDeviceMessage.replyOf( + received.getRequestId(), "thing.property.set_reply", null, 0, null); + String replyTopic = String.format("/sys/%s/%s/thing/property/set_reply", PRODUCT_KEY, DEVICE_NAME); + byte[] replyPayload = SERIALIZER.serialize(reply); + client.publish(replyTopic, Buffer.buffer(replyPayload), MqttQoS.AT_LEAST_ONCE, false, false); + log.info("[testSubscribe][已回复属性设置: topic={}]", replyTopic); + } catch (Exception e) { + log.error("[testSubscribe][回复属性设置异常]", e); + } + } + }); - // 3. 订阅下行主题 - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); + // 3. 订阅下行主题(属性设置 + 服务调用) + String topic = String.format("/sys/%s/%s/#", PRODUCT_KEY, DEVICE_NAME); log.info("[testSubscribe][订阅主题: {}]", topic); subscribe(client, topic); log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); diff --git a/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java b/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java index 14e91b6612..6f59f8d321 100644 --- a/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java +++ b/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java @@ -60,7 +60,7 @@ public class MpAccountServiceImpl implements MpAccountService { private MpAccountMapper mpAccountMapper; @Resource - @Lazy // 延迟加载,解决循环依赖的问题 + @Lazy // 延迟加载,解决循环依赖 private MpServiceFactory mpServiceFactory; @Override From baa779a271ec94017060c0f1da033c87e64d1343 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 10 Feb 2026 13:12:12 +0800 Subject: [PATCH 44/53] =?UTF-8?q?feat(iot):=20=E6=96=B0=E5=A2=9E=E4=BA=A7?= =?UTF-8?q?=E5=93=81=20TDengine=20=E8=A1=A8=E7=BB=93=E6=9E=84=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/product/IotProductController.http | 5 ++++ .../admin/product/IotProductController.java | 8 +++++++ .../dal/mysql/product/IotProductMapper.java | 4 ++++ .../service/product/IotProductService.java | 7 ++++++ .../product/IotProductServiceImpl.java | 23 +++++++++++++++++++ .../iot/core/enums/IotModbusModeEnum.java | 8 +++---- 6 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.http diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.http b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.http new file mode 100644 index 0000000000..e4e258985f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.http @@ -0,0 +1,5 @@ +### 请求 /iot/product/sync-property-table 接口 => 成功 +POST {{baseUrl}}/iot/product/sync-property-table +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java index 043f48772b..130c45d6a7 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java +++ b/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 { result.getData().getList()); } + @PostMapping("/sync-property-table") + @Operation(summary = "同步产品属性表结构到 TDengine") + @PreAuthorize("@ss.hasPermission('iot:product:update')") + public CommonResult syncProductPropertyTable() { + productService.syncProductPropertyTable(); + return success(true); + } + @GetMapping("/simple-list") @Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项") @Parameter(name = "deviceType", description = "设备类型", example = "1") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java index 2ed27dbb67..8c611d0d46 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java +++ b/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 { .apply("LOWER(product_key) = {0}", productKey.toLowerCase())); } + default List selectListByStatus(Integer status) { + return selectList(IotProductDO::getStatus, status); + } + default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) { return selectCount(new LambdaQueryWrapperX() .geIfPresent(IotProductDO::getCreateTime, createTime)); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java index 3d68fb59e1..f31961cfd1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java @@ -149,4 +149,11 @@ public interface IotProductService { */ void validateProductsExist(Collection ids); + /** + * 同步产品的 TDengine 表结构 + * + * 目的:当 MySQL 和 TDengine 不同步时,强制将已发布产品的表结构同步到 TDengine 中 + */ + void syncProductPropertyTable(); + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java index 49d7f36958..4c8a789b93 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java @@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; import com.baomidou.dynamic.datasource.annotation.DSTransactional; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Lazy; @@ -34,6 +35,7 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; * * @author ahh */ +@Slf4j @Service @Validated public class IotProductServiceImpl implements IotProductService { @@ -178,6 +180,27 @@ public class IotProductServiceImpl implements IotProductService { return productMapper.selectByIds(ids); } + @Override + public void syncProductPropertyTable() { + // 1. 获取所有已发布的产品 + List products = productMapper.selectListByStatus( + IotProductStatusEnum.PUBLISHED.getStatus()); + log.info("[syncProductPropertyTable][开始同步,已发布产品数量({})]", products.size()); + + // 2. 遍历同步 TDengine 表结构(创建产品超级表数据模型) + int successCount = 0; + for (IotProductDO product : products) { + try { + devicePropertyDataService.defineDevicePropertyData(product.getId()); + successCount++; + log.info("[syncProductPropertyTable][产品({}/{}) 同步成功]", product.getId(), product.getName()); + } catch (Exception e) { + log.error("[syncProductPropertyTable][产品({}/{}) 同步失败]", product.getId(), product.getName(), e); + } + } + log.info("[syncProductPropertyTable][同步完成,成功({}/{})个]", successCount, products.size()); + } + @Override public void validateProductsExist(Collection ids) { if (CollUtil.isEmpty(ids)) { diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusModeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusModeEnum.java index e8bcf2e68c..3736745918 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusModeEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusModeEnum.java @@ -7,7 +7,7 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; /** - * IoT Modbus 模式枚举 + * IoT Modbus 工作模式枚举 * * @author 芋道源码 */ @@ -16,18 +16,18 @@ import java.util.Arrays; public enum IotModbusModeEnum implements ArrayValuable { POLLING(1, "云端轮询"), - ACTIVE_REPORT(2, "主动上报"); + ACTIVE_REPORT(2, "边缘采集"); public static final Integer[] ARRAYS = Arrays.stream(values()) .map(IotModbusModeEnum::getMode) .toArray(Integer[]::new); /** - * 模式 + * 工作模式 */ private final Integer mode; /** - * 名称 + * 模式名称 */ private final String name; From 9031c06811832d5b117b9dc2d50b029d10de8f30 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 11 Feb 2026 20:16:19 +0800 Subject: [PATCH 45/53] =?UTF-8?q?feat(iot):=20=E8=B0=83=E6=95=B4=E4=B8=8B?= =?UTF-8?q?=E5=8C=85=E5=90=8D=EF=BC=8C=E9=99=8D=E4=BD=8E=E7=90=86=E8=A7=A3?= =?UTF-8?q?=E6=88=90=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/device/vo/device/IotDevicePageReqVO.java | 2 +- .../device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java | 4 ++-- .../admin/statistics/IotStatisticsController.java | 2 +- .../yudao/module/iot/dal/dataobject/device/IotDeviceDO.java | 6 +----- .../iot/dal/dataobject/device/IotDeviceModbusConfigDO.java | 6 ++++-- .../iot/dal/dataobject/device/IotDeviceModbusPointDO.java | 4 ++-- .../module/iot/job/device/IotDeviceOfflineCheckJob.java | 2 +- .../iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java | 2 +- .../iot/mq/consumer/device/IotDeviceMessageSubscriber.java | 2 +- ...essageHandler.java => IotDataRuleMessageSubscriber.java} | 2 +- ...ssageHandler.java => IotSceneRuleMessageSubscriber.java} | 2 +- .../yudao/module/iot/service/device/IotDeviceService.java | 2 +- .../module/iot/service/device/IotDeviceServiceImpl.java | 2 +- .../scene/IotSceneRuleTimerConditionIntegrationTest.java | 2 +- .../condition/IotDeviceStateConditionMatcherTest.java | 2 +- .../trigger/IotDeviceStateUpdateTriggerMatcherTest.java | 2 +- .../module/iot/core/biz/dto/IotModbusPointRespDTO.java | 4 ++-- .../iot/core/enums/{ => device}/IotDeviceStateEnum.java | 2 +- .../iot/core/enums/{ => modbus}/IotModbusByteOrderEnum.java | 2 +- .../core/enums/{ => modbus}/IotModbusFrameFormatEnum.java | 2 +- .../iot/core/enums/{ => modbus}/IotModbusModeEnum.java | 2 +- .../core/enums/{ => modbus}/IotModbusRawDataTypeEnum.java | 2 +- .../yudao/module/iot/core/mq/message/IotDeviceMessage.java | 2 +- .../protocol/modbus/common/utils/IotModbusCommonUtils.java | 4 ++-- .../manager/IotModbusTcpMasterConfigCacheService.java | 2 +- .../protocol/modbus/tcpslave/codec/IotModbusFrame.java | 2 +- .../modbus/tcpslave/codec/IotModbusFrameDecoder.java | 2 +- .../modbus/tcpslave/codec/IotModbusFrameEncoder.java | 2 +- .../downstream/IotModbusTcpSlaveDownstreamHandler.java | 2 +- .../handler/upstream/IotModbusTcpSlaveUpstreamHandler.java | 2 +- .../manager/IotModbusTcpSlaveConfigCacheService.java | 2 +- .../manager/IotModbusTcpSlaveConnectionManager.java | 2 +- .../manager/IotModbusTcpSlavePendingRequestManager.java | 2 +- .../tcpslave/manager/IotModbusTcpSlavePollScheduler.java | 2 +- .../tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java | 2 +- .../tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java | 2 +- 36 files changed, 43 insertions(+), 45 deletions(-) rename yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/{IotDataRuleMessageHandler.java => IotDataRuleMessageSubscriber.java} (93%) rename yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/{IotSceneRuleMessageHandler.java => IotSceneRuleMessageSubscriber.java} (92%) rename yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/{ => device}/IotDeviceStateEnum.java (94%) rename yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/{ => modbus}/IotModbusByteOrderEnum.java (95%) rename yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/{ => modbus}/IotModbusFrameFormatEnum.java (92%) rename yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/{ => modbus}/IotModbusModeEnum.java (93%) rename yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/{ => modbus}/IotModbusRawDataTypeEnum.java (96%) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java index e527242fb3..ddedb61354 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java +++ b/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; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java index 9b08d88555..f7f26dd428 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java index 22837c48ba..539ebbfc20 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java +++ b/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; import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO; import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsSummaryRespVO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java index 7b7d021c3b..0499ac09f4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java +++ b/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; import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; @@ -108,10 +108,6 @@ public class IotDeviceDO extends TenantBaseDO { */ private LocalDateTime activeTime; - /** - * 设备的 IP 地址 - */ - private String ip; /** * 固件编号 * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java index 0204908d9e..26981506b4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; @@ -61,13 +63,13 @@ public class IotDeviceModbusConfigDO extends TenantBaseDO { /** * 模式 * - * @see cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum + * @see IotModbusModeEnum */ private Integer mode; /** * 数据帧格式 * - * @see cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum + * @see IotModbusFrameFormatEnum */ private Integer frameFormat; /** diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java index 2937476cff..7eb2729888 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java index 6bd27a679a..789b2f25ad 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java +++ b/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; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.framework.iot.config.YudaoIotProperties; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java index 8a15c5e7bb..9aa2312117 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java +++ b/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; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java index c6e0ba4221..31c507889b 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java +++ b/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; import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotDataRuleMessageHandler.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotDataRuleMessageSubscriber.java similarity index 93% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotDataRuleMessageHandler.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotDataRuleMessageSubscriber.java index 843592a272..bbc4f5a443 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotDataRuleMessageHandler.java +++ b/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; */ @Component @Slf4j -public class IotDataRuleMessageHandler implements IotMessageSubscriber { +public class IotDataRuleMessageSubscriber implements IotMessageSubscriber { @Resource private IotDataRuleService dataRuleService; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageHandler.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageSubscriber.java similarity index 92% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageHandler.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageSubscriber.java index e889a3b127..de74bebcce 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageHandler.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageSubscriber.java @@ -16,7 +16,7 @@ import org.springframework.stereotype.Component; */ @Component @Slf4j -public class IotSceneRuleMessageHandler implements IotMessageSubscriber { +public class IotSceneRuleMessageSubscriber implements IotMessageSubscriber { @Resource private IotSceneRuleService sceneRuleService; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java index 5a622e5654..74339af6df 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java +++ b/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; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java index 148dd071e5..f05776f4cb 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java +++ b/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.*; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java index 7fcae15713..7f4ec70d6a 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java +++ b/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; import cn.hutool.core.collection.ListUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java index 6e7caecdd3..b83e0b0892 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java index f41b0ec590..79511aaa9c 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java index 0424f1bc16..dd6f9cf370 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.iot.core.biz.dto; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum; import lombok.Data; import java.math.BigDecimal; diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceStateEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/device/IotDeviceStateEnum.java similarity index 94% rename from yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceStateEnum.java rename to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/device/IotDeviceStateEnum.java index d0ff8357e7..fd8ca0e310 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceStateEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/device/IotDeviceStateEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.core.enums; +package cn.iocoder.yudao.module.iot.core.enums.device; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusByteOrderEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusByteOrderEnum.java similarity index 95% rename from yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusByteOrderEnum.java rename to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusByteOrderEnum.java index 17980840e9..229257a17a 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusByteOrderEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusByteOrderEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.core.enums; +package cn.iocoder.yudao.module.iot.core.enums.modbus; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusFrameFormatEnum.java similarity index 92% rename from yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java rename to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusFrameFormatEnum.java index 4e963850eb..bf1de5414b 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusFrameFormatEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.core.enums; +package cn.iocoder.yudao.module.iot.core.enums.modbus; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusModeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusModeEnum.java similarity index 93% rename from yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusModeEnum.java rename to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusModeEnum.java index 3736745918..ed4b3891e7 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusModeEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusModeEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.core.enums; +package cn.iocoder.yudao.module.iot.core.enums.modbus; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusRawDataTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusRawDataTypeEnum.java similarity index 96% rename from yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusRawDataTypeEnum.java rename to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusRawDataTypeEnum.java index 93e0b4a5e4..522b0aeafa 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusRawDataTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusRawDataTypeEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.core.enums; +package cn.iocoder.yudao.module.iot.core.enums.modbus; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java index 813b360433..830b5a0006 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.core.mq.message; import cn.hutool.core.map.MapUtil; import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java index 28889c321a..6a87334846 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java @@ -7,8 +7,8 @@ import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java index aa6273485f..46904ad49f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java @@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java index 347b3c5386..635a284020 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import lombok.Data; import lombok.experimental.Accessors; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java index 1415b51779..f333489aea 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java index 2a2bb3574a..d682c13767 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java index 900b01a47c..3867c4bedf 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java @@ -5,7 +5,7 @@ import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java index 4742610de9..448e1a9259 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java @@ -15,7 +15,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java index cf8589861b..e659a89864 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java @@ -6,7 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java index 16899c08fe..a0a7ad2949 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import io.vertx.core.Future; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java index 76e7d11e94..cea8f9862c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; import lombok.AllArgsConstructor; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java index 80bef684db..31bb8d5fda 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java @@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java index 286923377a..041c8c3cf8 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java @@ -4,7 +4,7 @@ import cn.hutool.core.util.HexUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java index 720887a914..6360254f83 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java @@ -4,7 +4,7 @@ import cn.hutool.core.util.HexUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; From fe8044c8a1fc5695c2d3fcf0bd58b98052b1d29c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 13 Feb 2026 15:36:17 +0800 Subject: [PATCH 46/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9AModbus?= =?UTF-8?q?=20=E6=94=AF=E6=8C=81=20Master/Slave=20=E5=8F=8C=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E9=85=8D=E7=BD=AE=E8=A1=A8=E5=8D=95=E5=92=8C?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E6=8C=89=E5=8D=8F=E8=AE=AE=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=8C=BA=E5=88=86=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../device/IotDeviceModbusPointDO.java | 6 -- .../IotDeviceModbusConfigServiceImpl.java | 16 ++--- .../iot/core/enums/IotProtocolTypeEnum.java | 4 +- .../gateway/config/IotGatewayProperties.java | 12 ++-- .../gateway/protocol/IotProtocolManager.java | 28 ++++---- .../common/utils/IotModbusCommonUtils.java | 2 +- ...tils.java => IotModbusTcpClientUtils.java} | 10 +-- .../IotModbusTcpClientConfig.java} | 6 +- .../IotModbusTcpClientProtocol.java} | 64 ++++++++--------- .../IotModbusTcpClientDownstreamHandler.java} | 25 ++++--- ...tModbusTcpClientDownstreamSubscriber.java} | 12 ++-- .../IotModbusTcpClientUpstreamHandler.java} | 6 +- ...IotModbusTcpClientConfigCacheService.java} | 8 +-- .../IotModbusTcpClientConnectionManager.java} | 10 +-- .../IotModbusTcpClientPollScheduler.java} | 28 ++++---- .../package-info.java | 4 +- .../IotModbusTcpServerConfig.java} | 6 +- .../IotModbusTcpServerProtocol.java} | 70 +++++++++---------- .../codec/IotModbusFrame.java | 2 +- .../codec/IotModbusFrameDecoder.java | 2 +- .../codec/IotModbusFrameEncoder.java | 2 +- .../IotModbusTcpServerDownstreamHandler.java} | 25 ++++--- ...tModbusTcpServerDownstreamSubscriber.java} | 14 ++-- .../IotModbusTcpServerUpstreamHandler.java} | 40 +++++------ ...IotModbusTcpServerConfigCacheService.java} | 10 +-- .../IotModbusTcpServerConnectionManager.java} | 6 +- ...ModbusTcpServerPendingRequestManager.java} | 8 +-- .../IotModbusTcpServerPollScheduler.java} | 26 +++---- .../{tcpslave => tcpserver}/package-info.java | 4 +- .../downstream/IotTcpDownstreamHandler.java | 12 +++- .../src/main/resources/application.yaml | 16 ++--- .../IoTModbusTcpClientIntegrationTest.java} | 10 +-- ...IotModbusTcpServerRtuIntegrationTest.java} | 18 ++--- ...IotModbusTcpServerTcpIntegrationTest.java} | 18 ++--- 34 files changed, 270 insertions(+), 260 deletions(-) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/{IotModbusTcpMasterUtils.java => IotModbusTcpClientUtils.java} (96%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpmaster/IotModbusTcpMasterConfig.java => tcpclient/IotModbusTcpClientConfig.java} (71%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpmaster/IotModbusTcpMasterProtocol.java => tcpclient/IotModbusTcpClientProtocol.java} (69%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamHandler.java => tcpclient/handler/downstream/IotModbusTcpClientDownstreamHandler.java} (80%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamSubscriber.java => tcpclient/handler/downstream/IotModbusTcpClientDownstreamSubscriber.java} (59%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpmaster/handler/upstream/IotModbusTcpMasterUpstreamHandler.java => tcpclient/handler/upstream/IotModbusTcpClientUpstreamHandler.java} (92%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java => tcpclient/manager/IotModbusTcpClientConfigCacheService.java} (93%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpmaster/manager/IotModbusTcpMasterConnectionManager.java => tcpclient/manager/IotModbusTcpClientConnectionManager.java} (96%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpmaster/manager/IotModbusTcpMasterPollScheduler.java => tcpclient/manager/IotModbusTcpClientPollScheduler.java} (72%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpmaster => tcpclient}/package-info.java (50%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave/IotModbusTcpSlaveConfig.java => tcpserver/IotModbusTcpServerConfig.java} (93%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave/IotModbusTcpSlaveProtocol.java => tcpserver/IotModbusTcpServerProtocol.java} (77%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave => tcpserver}/codec/IotModbusFrame.java (99%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave => tcpserver}/codec/IotModbusFrameDecoder.java (99%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave => tcpserver}/codec/IotModbusFrameEncoder.java (99%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java => tcpserver/handler/downstream/IotModbusTcpServerDownstreamHandler.java} (85%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java => tcpserver/handler/downstream/IotModbusTcpServerDownstreamSubscriber.java} (53%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java => tcpserver/handler/upstream/IotModbusTcpServerUpstreamHandler.java} (87%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java => tcpserver/manager/IotModbusTcpServerConfigCacheService.java} (95%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave/manager/IotModbusTcpSlaveConnectionManager.java => tcpserver/manager/IotModbusTcpServerConnectionManager.java} (98%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java => tcpserver/manager/IotModbusTcpServerPendingRequestManager.java} (95%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave/manager/IotModbusTcpSlavePollScheduler.java => tcpserver/manager/IotModbusTcpServerPollScheduler.java} (78%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave => tcpserver}/package-info.java (56%) rename yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpmaster/IoTModbusTcpMasterIntegrationTest.java => tcpclient/IoTModbusTcpClientIntegrationTest.java} (94%) rename yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java => tcpserver/IotModbusTcpServerRtuIntegrationTest.java} (95%) rename yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/{tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java => tcpserver/IotModbusTcpServerTcpIntegrationTest.java} (95%) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java index 7eb2729888..cfc084166a 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java @@ -29,12 +29,6 @@ public class IotDeviceModbusPointDO extends TenantBaseDO { */ @TableId private Long id; - /** - * 产品编号 - * - * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} - */ - private Long productId; /** * 设备编号 * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java index a97bcb14d2..2a8ae4e439 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java @@ -75,14 +75,14 @@ public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigSe if (protocolTypeEnum == null) { return; } - if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_MASTER) { - Assert.isTrue(StrUtil.isNotEmpty(saveReqVO.getIp()), "Master 模式下,IP 地址不能为空"); - Assert.notNull(saveReqVO.getPort(), "Master 模式下,端口不能为空"); - Assert.notNull(saveReqVO.getTimeout(), "Master 模式下,连接超时时间不能为空"); - Assert.notNull(saveReqVO.getRetryInterval(), "Master 模式下,重试间隔不能为空"); - } else if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_SLAVE) { - Assert.notNull(saveReqVO.getMode(), "Slave 模式下,工作模式不能为空"); - Assert.notNull(saveReqVO.getFrameFormat(), "Slave 模式下,数据帧格式不能为空"); + if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_CLIENT) { + Assert.isTrue(StrUtil.isNotEmpty(saveReqVO.getIp()), "Client 模式下,IP 地址不能为空"); + Assert.notNull(saveReqVO.getPort(), "Client 模式下,端口不能为空"); + Assert.notNull(saveReqVO.getTimeout(), "Client 模式下,连接超时时间不能为空"); + Assert.notNull(saveReqVO.getRetryInterval(), "Client 模式下,重试间隔不能为空"); + } else if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_SERVER) { + Assert.notNull(saveReqVO.getMode(), "Server 模式下,工作模式不能为空"); + Assert.notNull(saveReqVO.getFrameFormat(), "Server 模式下,数据帧格式不能为空"); } } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java index e299364755..753605426b 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java @@ -25,8 +25,8 @@ public enum IotProtocolTypeEnum implements ArrayValuable { MQTT("mqtt"), EMQX("emqx"), COAP("coap"), - MODBUS_TCP_MASTER("modbus_tcp_master"), - MODBUS_TCP_SLAVE("modbus_tcp_slave"); + MODBUS_TCP_CLIENT("modbus_tcp_client"), + MODBUS_TCP_SERVER("modbus_tcp_server"); public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 8fcc3eae39..63894dc9df 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -4,8 +4,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterConfig; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; @@ -169,16 +169,16 @@ public class IotGatewayProperties { private IotEmqxConfig emqx; /** - * Modbus TCP Master 协议配置 + * Modbus TCP Client 协议配置 */ @Valid - private IotModbusTcpMasterConfig modbusTcpMaster; + private IotModbusTcpClientConfig modbusTcpClient; /** - * Modbus TCP Slave 协议配置 + * Modbus TCP Server 协议配置 */ @Valid - private IotModbusTcpSlaveConfig modbusTcpSlave; + private IotModbusTcpServerConfig modbusTcpServer; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index eb9fd06cf1..310533d22e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; @@ -114,10 +114,10 @@ public class IotProtocolManager implements SmartLifecycle { return createMqttProtocol(config); case EMQX: return createEmqxProtocol(config); - case MODBUS_TCP_MASTER: - return createModbusTcpMasterProtocol(config); - case MODBUS_TCP_SLAVE: - return createModbusTcpSlaveProtocol(config); + case MODBUS_TCP_CLIENT: + return createModbusTcpClientProtocol(config); + case MODBUS_TCP_SERVER: + return createModbusTcpServerProtocol(config); default: throw new IllegalArgumentException(String.format( "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); @@ -195,23 +195,23 @@ public class IotProtocolManager implements SmartLifecycle { } /** - * 创建 Modbus TCP Master 协议实例 + * 创建 Modbus TCP Client 协议实例 * * @param config 协议实例配置 - * @return Modbus TCP Master 协议实例 + * @return Modbus TCP Client 协议实例 */ - private IotModbusTcpMasterProtocol createModbusTcpMasterProtocol(IotGatewayProperties.ProtocolProperties config) { - return new IotModbusTcpMasterProtocol(config); + private IotModbusTcpClientProtocol createModbusTcpClientProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotModbusTcpClientProtocol(config); } /** - * 创建 Modbus TCP Slave 协议实例 + * 创建 Modbus TCP Server 协议实例 * * @param config 协议实例配置 - * @return Modbus TCP Slave 协议实例 + * @return Modbus TCP Server 协议实例 */ - private IotModbusTcpSlaveProtocol createModbusTcpSlaveProtocol(IotGatewayProperties.ProtocolProperties config) { - return new IotModbusTcpSlaveProtocol(config); + private IotModbusTcpServerProtocol createModbusTcpServerProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotModbusTcpServerProtocol(config); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java index 6a87334846..312e796df1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java @@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java similarity index 96% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java index 1f7c14dc41..1324f3aa5a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager; import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; import com.ghgande.j2mod.modbus.msg.*; import com.ghgande.j2mod.modbus.procimg.InputRegister; @@ -19,14 +19,14 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.I *

                              * 封装基于 j2mod 的 Modbus TCP 读写操作: * 1. 根据功能码创建对应的 Modbus 读/写请求 - * 2. 通过 {@link IotModbusTcpMasterConnectionManager.ModbusConnection} 执行事务 + * 2. 通过 {@link IotModbusTcpClientConnectionManager.ModbusConnection} 执行事务 * 3. 从响应中提取原始值 * * @author 芋道源码 */ @UtilityClass @Slf4j -public class IotModbusTcpMasterUtils { +public class IotModbusTcpClientUtils { /** * 读取 Modbus 数据 @@ -36,7 +36,7 @@ public class IotModbusTcpMasterUtils { * @param point 点位配置 * @return 原始值(int 数组) */ - public static Future read(IotModbusTcpMasterConnectionManager.ModbusConnection connection, + public static Future read(IotModbusTcpClientConnectionManager.ModbusConnection connection, Integer slaveId, IotModbusPointRespDTO point) { return connection.executeBlocking(tcpConnection -> { @@ -70,7 +70,7 @@ public class IotModbusTcpMasterUtils { * @param values 要写入的值 * @return 是否成功 */ - public static Future write(IotModbusTcpMasterConnectionManager.ModbusConnection connection, + public static Future write(IotModbusTcpClientConnectionManager.ModbusConnection connection, Integer slaveId, IotModbusPointRespDTO point, int[] values) { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientConfig.java similarity index 71% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterConfig.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientConfig.java index 9b4867c360..1743140953 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientConfig.java @@ -1,16 +1,16 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.Data; /** - * IoT Modbus TCP Master 协议配置 + * IoT Modbus TCP Client 协议配置 * * @author 芋道源码 */ @Data -public class IotModbusTcpMasterConfig { +public class IotModbusTcpClientConfig { /** * 配置刷新间隔(秒) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientProtocol.java similarity index 69% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientProtocol.java index 31271b6350..f632b62fee 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientProtocol.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient; import cn.hutool.core.lang.Assert; import cn.hutool.extra.spring.SpringUtil; @@ -9,12 +9,12 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpMasterDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpMasterDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpMasterUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientPollScheduler; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; import lombok.Getter; @@ -26,12 +26,12 @@ import java.util.Set; import java.util.concurrent.TimeUnit; /** - * IoT 网关 Modbus TCP Master 协议:主动轮询 Modbus 从站设备数据 + * IoT 网关 Modbus TCP Client 协议:主动轮询 Modbus 从站设备数据 * * @author 芋道源码 */ @Slf4j -public class IotModbusTcpMasterProtocol implements IotProtocol { +public class IotModbusTcpClientProtocol implements IotProtocol { /** * 协议配置 @@ -61,18 +61,18 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { /** * 连接管理器 */ - private final IotModbusTcpMasterConnectionManager connectionManager; + private final IotModbusTcpClientConnectionManager connectionManager; /** * 下行消息订阅者 */ - private IotModbusTcpMasterDownstreamSubscriber downstreamSubscriber; + private IotModbusTcpClientDownstreamSubscriber downstreamSubscriber; - private final IotModbusTcpMasterConfigCacheService configCacheService; - private final IotModbusTcpMasterPollScheduler pollScheduler; + private final IotModbusTcpClientConfigCacheService configCacheService; + private final IotModbusTcpClientPollScheduler pollScheduler; - public IotModbusTcpMasterProtocol(ProtocolProperties properties) { - IotModbusTcpMasterConfig modbusTcpMasterConfig = properties.getModbusTcpMaster(); - Assert.notNull(modbusTcpMasterConfig, "Modbus TCP Master 协议配置(modbusTcpMaster)不能为空"); + public IotModbusTcpClientProtocol(ProtocolProperties properties) { + IotModbusTcpClientConfig modbusTcpClientConfig = properties.getModbusTcpClient(); + Assert.notNull(modbusTcpClientConfig, "Modbus TCP Client 协议配置(modbusTcpClient)不能为空"); this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); @@ -83,15 +83,15 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class); IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); - this.configCacheService = new IotModbusTcpMasterConfigCacheService(deviceApi); - this.connectionManager = new IotModbusTcpMasterConnectionManager(redissonClient, vertx, + this.configCacheService = new IotModbusTcpClientConfigCacheService(deviceApi); + this.connectionManager = new IotModbusTcpClientConnectionManager(redissonClient, vertx, messageService, configCacheService, serverId); // 初始化 Handler - IotModbusTcpMasterUpstreamHandler upstreamHandler = new IotModbusTcpMasterUpstreamHandler(messageService, serverId); + IotModbusTcpClientUpstreamHandler upstreamHandler = new IotModbusTcpClientUpstreamHandler(messageService, serverId); // 初始化轮询调度器 - this.pollScheduler = new IotModbusTcpMasterPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService); + this.pollScheduler = new IotModbusTcpClientPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService); } @Override @@ -101,13 +101,13 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { @Override public IotProtocolTypeEnum getType() { - return IotProtocolTypeEnum.MODBUS_TCP_MASTER; + return IotProtocolTypeEnum.MODBUS_TCP_CLIENT; } @Override public void start() { if (running) { - log.warn("[start][IoT Modbus TCP Master 协议 {} 已经在运行中]", getId()); + log.warn("[start][IoT Modbus TCP Client 协议 {} 已经在运行中]", getId()); return; } @@ -115,22 +115,22 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { // 1.1 首次加载配置 refreshConfig(); // 1.2 启动配置刷新定时器 - int refreshInterval = properties.getModbusTcpMaster().getConfigRefreshInterval(); + int refreshInterval = properties.getModbusTcpClient().getConfigRefreshInterval(); configRefreshTimerId = vertx.setPeriodic( TimeUnit.SECONDS.toMillis(refreshInterval), id -> refreshConfig() ); running = true; - log.info("[start][IoT Modbus TCP Master 协议 {} 启动成功,serverId={}]", getId(), serverId); + log.info("[start][IoT Modbus TCP Client 协议 {} 启动成功,serverId={}]", getId(), serverId); // 2. 启动下行消息订阅者 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - IotModbusTcpMasterDownstreamHandler downstreamHandler = new IotModbusTcpMasterDownstreamHandler(connectionManager, + IotModbusTcpClientDownstreamHandler downstreamHandler = new IotModbusTcpClientDownstreamHandler(connectionManager, configCacheService); - this.downstreamSubscriber = new IotModbusTcpMasterDownstreamSubscriber(this, downstreamHandler, messageBus); + this.downstreamSubscriber = new IotModbusTcpClientDownstreamSubscriber(this, downstreamHandler, messageBus); this.downstreamSubscriber.start(); } catch (Exception e) { - log.error("[start][IoT Modbus TCP Master 协议 {} 启动失败]", getId(), e); + log.error("[start][IoT Modbus TCP Client 协议 {} 启动失败]", getId(), e); stop0(); throw e; } @@ -149,9 +149,9 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { if (downstreamSubscriber != null) { try { downstreamSubscriber.stop(); - log.info("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者已停止]", getId()); + log.info("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者已停止]", getId()); } catch (Exception e) { - log.error("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者停止失败]", getId(), e); + log.error("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者停止失败]", getId(), e); } downstreamSubscriber = null; } @@ -170,13 +170,13 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { if (vertx != null) { try { vertx.close().result(); - log.info("[stop][IoT Modbus TCP Master 协议 {} Vertx 已关闭]", getId()); + log.info("[stop][IoT Modbus TCP Client 协议 {} Vertx 已关闭]", getId()); } catch (Exception e) { - log.error("[stop][IoT Modbus TCP Master 协议 {} Vertx 关闭失败]", getId(), e); + log.error("[stop][IoT Modbus TCP Client 协议 {} Vertx 关闭失败]", getId(), e); } } running = false; - log.info("[stop][IoT Modbus TCP Master 协议 {} 已停止]", getId()); + log.info("[stop][IoT Modbus TCP Client 协议 {} 已停止]", getId()); } /** diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamHandler.java similarity index 80% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamHandler.java index af51e22933..045e61fdf2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream; import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; @@ -6,16 +6,16 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpMasterUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import java.util.Map; /** - * IoT Modbus TCP Master 下行消息处理器 + * IoT Modbus TCP Client 下行消息处理器 *

                              * 负责: * 1. 处理下行消息(如属性设置 thing.service.property.set) @@ -25,10 +25,10 @@ import java.util.Map; */ @RequiredArgsConstructor @Slf4j -public class IotModbusTcpMasterDownstreamHandler { +public class IotModbusTcpClientDownstreamHandler { - private final IotModbusTcpMasterConnectionManager connectionManager; - private final IotModbusTcpMasterConfigCacheService configCacheService; + private final IotModbusTcpClientConnectionManager connectionManager; + private final IotModbusTcpClientConfigCacheService configCacheService; /** * 处理下行消息 @@ -36,8 +36,11 @@ public class IotModbusTcpMasterDownstreamHandler { @SuppressWarnings({"unchecked", "DuplicatedCode"}) public void handle(IotDeviceMessage message) { // 1.1 检查是否是属性设置消息 + if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) { + return; + } if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { - log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); + log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod()); return; } // 1.2 获取设备配置 @@ -79,7 +82,7 @@ public class IotModbusTcpMasterDownstreamHandler { */ private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) { // 1.1 获取连接 - IotModbusTcpMasterConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); + IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); if (connection == null) { log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId()); return; @@ -94,7 +97,7 @@ public class IotModbusTcpMasterDownstreamHandler { // 2.1 转换属性值为原始值 int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point); // 2.2 执行 Modbus 写入 - IotModbusTcpMasterUtils.write(connection, slaveId, point, rawValues) + IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues) .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", config.getDeviceId(), point.getIdentifier(), value)) .onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]", diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamSubscriber.java similarity index 59% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamSubscriber.java index 812e13da01..6c8a4be9bb 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamSubscriber.java @@ -1,9 +1,9 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol; import lombok.extern.slf4j.Slf4j; /** @@ -12,12 +12,12 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j -public class IotModbusTcpMasterDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { +public class IotModbusTcpClientDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { - private final IotModbusTcpMasterDownstreamHandler downstreamHandler; + private final IotModbusTcpClientDownstreamHandler downstreamHandler; - public IotModbusTcpMasterDownstreamSubscriber(IotModbusTcpMasterProtocol protocol, - IotModbusTcpMasterDownstreamHandler downstreamHandler, + public IotModbusTcpClientDownstreamSubscriber(IotModbusTcpClientProtocol protocol, + IotModbusTcpClientDownstreamHandler downstreamHandler, IotMessageBus messageBus) { super(protocol, messageBus); this.downstreamHandler = downstreamHandler; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpMasterUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/upstream/IotModbusTcpClientUpstreamHandler.java similarity index 92% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpMasterUpstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/upstream/IotModbusTcpClientUpstreamHandler.java index 7fc573d950..2b7e9c206a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpMasterUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/upstream/IotModbusTcpClientUpstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream; import cn.hutool.core.map.MapUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; @@ -17,13 +17,13 @@ import java.util.Map; * @author 芋道源码 */ @Slf4j -public class IotModbusTcpMasterUpstreamHandler { +public class IotModbusTcpClientUpstreamHandler { private final IotDeviceMessageService messageService; private final String serverId; - public IotModbusTcpMasterUpstreamHandler(IotDeviceMessageService messageService, + public IotModbusTcpClientUpstreamHandler(IotDeviceMessageService messageService, String serverId) { this.messageService = messageService; this.serverId = serverId; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConfigCacheService.java similarity index 93% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConfigCacheService.java index 46904ad49f..b28e507c45 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConfigCacheService.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -19,13 +19,13 @@ import java.util.concurrent.ConcurrentHashMap; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; /** - * IoT Modbus TCP Master 配置缓存服务 + * IoT Modbus TCP Client 配置缓存服务 * * @author 芋道源码 */ @RequiredArgsConstructor @Slf4j -public class IotModbusTcpMasterConfigCacheService { +public class IotModbusTcpClientConfigCacheService { private final IotDeviceCommonApi deviceApi; @@ -51,7 +51,7 @@ public class IotModbusTcpMasterConfigCacheService { // 1. 从远程获取配置 CommonResult> result = deviceApi.getModbusDeviceConfigList( new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus()) - .setMode(IotModbusModeEnum.POLLING.getMode()).setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_MASTER.getType())); + .setMode(IotModbusModeEnum.POLLING.getMode()).setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_CLIENT.getType())); result.checkError(); List configs = result.getData(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConnectionManager.java similarity index 96% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConnectionManager.java index 4fb605a932..bfdacd020d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConnectionManager.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager; import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; @@ -30,14 +30,14 @@ import java.util.concurrent.ConcurrentHashMap; * @author 芋道源码 */ @Slf4j -public class IotModbusTcpMasterConnectionManager { +public class IotModbusTcpClientConnectionManager { private static final String LOCK_KEY_PREFIX = "iot:modbus-tcp:connection:"; private final RedissonClient redissonClient; private final Vertx vertx; private final IotDeviceMessageService messageService; - private final IotModbusTcpMasterConfigCacheService configCacheService; + private final IotModbusTcpClientConfigCacheService configCacheService; private final String serverId; /** @@ -50,9 +50,9 @@ public class IotModbusTcpMasterConnectionManager { */ private final Map deviceConnectionMap = new ConcurrentHashMap<>(); - public IotModbusTcpMasterConnectionManager(RedissonClient redissonClient, Vertx vertx, + public IotModbusTcpClientConnectionManager(RedissonClient redissonClient, Vertx vertx, IotDeviceMessageService messageService, - IotModbusTcpMasterConfigCacheService configCacheService, + IotModbusTcpClientConfigCacheService configCacheService, String serverId) { this.redissonClient = redissonClient; this.vertx = vertx; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java similarity index 72% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterPollScheduler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java index df87b91fa1..946937d405 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterPollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; @@ -6,27 +6,27 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpMasterUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpMasterUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler; import io.vertx.core.Vertx; import lombok.extern.slf4j.Slf4j; /** - * IoT Modbus TCP Master 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 + * IoT Modbus TCP Client 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 * * @author 芋道源码 */ @Slf4j -public class IotModbusTcpMasterPollScheduler extends AbstractIotModbusPollScheduler { +public class IotModbusTcpClientPollScheduler extends AbstractIotModbusPollScheduler { - private final IotModbusTcpMasterConnectionManager connectionManager; - private final IotModbusTcpMasterUpstreamHandler upstreamHandler; - private final IotModbusTcpMasterConfigCacheService configCacheService; + private final IotModbusTcpClientConnectionManager connectionManager; + private final IotModbusTcpClientUpstreamHandler upstreamHandler; + private final IotModbusTcpClientConfigCacheService configCacheService; - public IotModbusTcpMasterPollScheduler(Vertx vertx, - IotModbusTcpMasterConnectionManager connectionManager, - IotModbusTcpMasterUpstreamHandler upstreamHandler, - IotModbusTcpMasterConfigCacheService configCacheService) { + public IotModbusTcpClientPollScheduler(Vertx vertx, + IotModbusTcpClientConnectionManager connectionManager, + IotModbusTcpClientUpstreamHandler upstreamHandler, + IotModbusTcpClientConfigCacheService configCacheService) { super(vertx); this.connectionManager = connectionManager; this.upstreamHandler = upstreamHandler; @@ -54,7 +54,7 @@ public class IotModbusTcpMasterPollScheduler extends AbstractIotModbusPollSchedu } // 2.1 获取连接 - IotModbusTcpMasterConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId); + IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId); if (connection == null) { log.warn("[pollPoint][设备 {} 没有连接]", deviceId); return; @@ -64,7 +64,7 @@ public class IotModbusTcpMasterPollScheduler extends AbstractIotModbusPollSchedu Assert.notNull(slaveId, "设备 {} 没有配置 slaveId", deviceId); // 3. 执行 Modbus 读取 - IotModbusTcpMasterUtils.read(connection, slaveId, point) + IotModbusTcpClientUtils.read(connection, slaveId, point) .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue)) .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]", deviceId, point.getIdentifier(), e)); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/package-info.java similarity index 50% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/package-info.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/package-info.java index 86e393233a..3bd4fa95f4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/package-info.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/package-info.java @@ -1,6 +1,6 @@ /** - * Modbus TCP Master(主站)协议:网关主动连接并轮询 Modbus 从站设备 + * Modbus TCP Client(主站)协议:网关主动连接并轮询 Modbus 从站设备 *

                              * 基于 j2mod 实现,支持 FC01-04 读、FC05/06/15/16 写,定时轮询 + 下发属性设置 */ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerConfig.java similarity index 93% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerConfig.java index 60185f1eb0..5b4fd22362 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerConfig.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; @@ -6,12 +6,12 @@ import jakarta.validation.constraints.NotNull; import lombok.Data; /** - * IoT Modbus TCP Slave 协议配置 + * IoT Modbus TCP Server 协议配置 * * @author 芋道源码 */ @Data -public class IotModbusTcpSlaveConfig { +public class IotModbusTcpServerConfig { /** * 配置刷新间隔(秒) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java similarity index 77% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java index feb89811db..80ce9eec08 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; @@ -11,16 +11,16 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream.IotModbusTcpSlaveUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.upstream.IotModbusTcpServerUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPollScheduler; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; @@ -37,7 +37,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** - * IoT 网关 Modbus TCP Slave 协议 + * IoT 网关 Modbus TCP Server 协议 *

                              * 作为 TCP Server 接收设备主动连接: * 1. 设备通过自定义功能码(FC 65)发送认证请求 @@ -46,7 +46,7 @@ import java.util.concurrent.atomic.AtomicInteger; * @author 芋道源码 */ @Slf4j -public class IotModbusTcpSlaveProtocol implements IotProtocol { +public class IotModbusTcpServerProtocol implements IotProtocol { /** * 协议配置 @@ -84,25 +84,25 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { /** * 连接管理器 */ - private final IotModbusTcpSlaveConnectionManager connectionManager; + private final IotModbusTcpServerConnectionManager connectionManager; /** * 下行消息订阅者 */ - private IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; + private IotModbusTcpServerDownstreamSubscriber downstreamSubscriber; private final IotModbusFrameDecoder frameDecoder; @SuppressWarnings("FieldCanBeLocal") private final IotModbusFrameEncoder frameEncoder; - private final IotModbusTcpSlaveConfigCacheService configCacheService; - private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; - private final IotModbusTcpSlaveUpstreamHandler upstreamHandler; - private final IotModbusTcpSlavePollScheduler pollScheduler; + private final IotModbusTcpServerConfigCacheService configCacheService; + private final IotModbusTcpServerPendingRequestManager pendingRequestManager; + private final IotModbusTcpServerUpstreamHandler upstreamHandler; + private final IotModbusTcpServerPollScheduler pollScheduler; private final IotDeviceMessageService messageService; - public IotModbusTcpSlaveProtocol(ProtocolProperties properties) { - IotModbusTcpSlaveConfig slaveConfig = properties.getModbusTcpSlave(); - Assert.notNull(slaveConfig, "Modbus TCP Slave 协议配置(modbusTcpSlave)不能为空"); + public IotModbusTcpServerProtocol(ProtocolProperties properties) { + IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer(); + Assert.notNull(slaveConfig, "Modbus TCP Server 协议配置(modbusTcpServer)不能为空"); this.properties = properties; this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); @@ -111,9 +111,9 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { // 初始化 Manager IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.connectionManager = new IotModbusTcpSlaveConnectionManager(); - this.configCacheService = new IotModbusTcpSlaveConfigCacheService(deviceApi); - this.pendingRequestManager = new IotModbusTcpSlavePendingRequestManager(); + this.connectionManager = new IotModbusTcpServerConnectionManager(); + this.configCacheService = new IotModbusTcpServerConfigCacheService(deviceApi); + this.pendingRequestManager = new IotModbusTcpServerPendingRequestManager(); // 初始化帧编解码器 this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode()); @@ -122,14 +122,14 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { // 初始化共享事务 ID 自增器(PollScheduler 和 DownstreamHandler 共用,避免 transactionId 冲突) AtomicInteger transactionIdCounter = new AtomicInteger(0); // 初始化轮询调度器 - this.pollScheduler = new IotModbusTcpSlavePollScheduler( + this.pollScheduler = new IotModbusTcpServerPollScheduler( vertx, connectionManager, frameEncoder, pendingRequestManager, slaveConfig.getRequestTimeout(), transactionIdCounter, configCacheService); // 初始化 Handler this.messageService = SpringUtil.getBean(IotDeviceMessageService.class); IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); - this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler( + this.upstreamHandler = new IotModbusTcpServerUpstreamHandler( deviceApi, this.messageService, frameEncoder, connectionManager, configCacheService, pendingRequestManager, pollScheduler, deviceService, serverId); @@ -142,19 +142,19 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { @Override public IotProtocolTypeEnum getType() { - return IotProtocolTypeEnum.MODBUS_TCP_SLAVE; + return IotProtocolTypeEnum.MODBUS_TCP_SERVER; } @Override public void start() { if (running) { - log.warn("[start][IoT Modbus TCP Slave 协议 {} 已经在运行中]", getId()); + log.warn("[start][IoT Modbus TCP Server 协议 {} 已经在运行中]", getId()); return; } try { // 1. 启动配置刷新定时器 - IotModbusTcpSlaveConfig slaveConfig = properties.getModbusTcpSlave(); + IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer(); configRefreshTimerId = vertx.setPeriodic( TimeUnit.SECONDS.toMillis(slaveConfig.getConfigRefreshInterval()), id -> refreshConfig()); @@ -167,18 +167,18 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { slaveConfig.getRequestCleanupInterval(), id -> pendingRequestManager.cleanupExpired()); running = true; - log.info("[start][IoT Modbus TCP Slave 协议 {} 启动成功, serverId={}, port={}]", + log.info("[start][IoT Modbus TCP Server 协议 {} 启动成功, serverId={}, port={}]", getId(), serverId, properties.getPort()); // 3. 启动下行消息订阅 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - IotModbusTcpSlaveDownstreamHandler downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( + IotModbusTcpServerDownstreamHandler downstreamHandler = new IotModbusTcpServerDownstreamHandler( connectionManager, configCacheService, frameEncoder, this.pollScheduler.getTransactionIdCounter()); - this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber( + this.downstreamSubscriber = new IotModbusTcpServerDownstreamSubscriber( this, downstreamHandler, messageBus); downstreamSubscriber.start(); } catch (Exception e) { - log.error("[start][IoT Modbus TCP Slave 协议 {} 启动失败]", getId(), e); + log.error("[start][IoT Modbus TCP Server 协议 {} 启动失败]", getId(), e); stop0(); throw e; } @@ -238,7 +238,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { } } running = false; - log.info("[stop][IoT Modbus TCP Slave 协议 {} 已停止]", getId()); + log.info("[stop][IoT Modbus TCP Server 协议 {} 已停止]", getId()); } /** diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrame.java similarity index 99% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrame.java index 635a284020..7aeca99182 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrame.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameDecoder.java similarity index 99% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameDecoder.java index f333489aea..5b92c3ea7f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameDecoder.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameEncoder.java similarity index 99% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameEncoder.java index d682c13767..b5c48b225b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameEncoder.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamHandler.java similarity index 85% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamHandler.java index 3867c4bedf..eb4683fc85 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; @@ -8,17 +8,17 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo; import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; /** - * IoT Modbus TCP Slave 下行消息处理器 + * IoT Modbus TCP Server 下行消息处理器 *

                              * 负责: * 1. 处理下行消息(如属性设置 thing.service.property.set) @@ -27,10 +27,10 @@ import java.util.concurrent.atomic.AtomicInteger; * @author 芋道源码 */ @Slf4j -public class IotModbusTcpSlaveDownstreamHandler { +public class IotModbusTcpServerDownstreamHandler { - private final IotModbusTcpSlaveConnectionManager connectionManager; - private final IotModbusTcpSlaveConfigCacheService configCacheService; + private final IotModbusTcpServerConnectionManager connectionManager; + private final IotModbusTcpServerConfigCacheService configCacheService; private final IotModbusFrameEncoder frameEncoder; /** @@ -38,8 +38,8 @@ public class IotModbusTcpSlaveDownstreamHandler { */ private final AtomicInteger transactionIdCounter; - public IotModbusTcpSlaveDownstreamHandler(IotModbusTcpSlaveConnectionManager connectionManager, - IotModbusTcpSlaveConfigCacheService configCacheService, + public IotModbusTcpServerDownstreamHandler(IotModbusTcpServerConnectionManager connectionManager, + IotModbusTcpServerConfigCacheService configCacheService, IotModbusFrameEncoder frameEncoder, AtomicInteger transactionIdCounter) { this.connectionManager = connectionManager; @@ -54,6 +54,9 @@ public class IotModbusTcpSlaveDownstreamHandler { @SuppressWarnings({"unchecked", "DuplicatedCode"}) public void handle(IotDeviceMessage message) { // 1.1 检查是否是属性设置消息 + if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) { + return; + } if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); return; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamSubscriber.java similarity index 53% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamSubscriber.java index 2a11cff565..1d36b69eee 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamSubscriber.java @@ -1,23 +1,23 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol; import lombok.extern.slf4j.Slf4j; /** - * IoT Modbus TCP Slave 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 + * IoT Modbus TCP Server 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 * * @author 芋道源码 */ @Slf4j -public class IotModbusTcpSlaveDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { +public class IotModbusTcpServerDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { - private final IotModbusTcpSlaveDownstreamHandler downstreamHandler; + private final IotModbusTcpServerDownstreamHandler downstreamHandler; - public IotModbusTcpSlaveDownstreamSubscriber(IotModbusTcpSlaveProtocol protocol, - IotModbusTcpSlaveDownstreamHandler downstreamHandler, + public IotModbusTcpServerDownstreamSubscriber(IotModbusTcpServerProtocol protocol, + IotModbusTcpServerDownstreamHandler downstreamHandler, IotMessageBus messageBus) { super(protocol, messageBus); this.downstreamHandler = downstreamHandler; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/upstream/IotModbusTcpServerUpstreamHandler.java similarity index 87% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/upstream/IotModbusTcpServerUpstreamHandler.java index 448e1a9259..db8a9fdfa2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/upstream/IotModbusTcpServerUpstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; @@ -20,14 +20,14 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager.PendingRequest; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPollScheduler; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.net.NetSocket; @@ -40,7 +40,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; /** - * IoT Modbus TCP Slave 上行数据处理器 + * IoT Modbus TCP Server 上行数据处理器 *

                              * 处理: * 1. 自定义 FC 认证 @@ -49,28 +49,28 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU * @author 芋道源码 */ @Slf4j -public class IotModbusTcpSlaveUpstreamHandler { +public class IotModbusTcpServerUpstreamHandler { private static final String METHOD_AUTH = "auth"; private final IotDeviceCommonApi deviceApi; private final IotDeviceMessageService messageService; private final IotModbusFrameEncoder frameEncoder; - private final IotModbusTcpSlaveConnectionManager connectionManager; - private final IotModbusTcpSlaveConfigCacheService configCacheService; - private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; - private final IotModbusTcpSlavePollScheduler pollScheduler; + private final IotModbusTcpServerConnectionManager connectionManager; + private final IotModbusTcpServerConfigCacheService configCacheService; + private final IotModbusTcpServerPendingRequestManager pendingRequestManager; + private final IotModbusTcpServerPollScheduler pollScheduler; private final IotDeviceService deviceService; private final String serverId; - public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi, + public IotModbusTcpServerUpstreamHandler(IotDeviceCommonApi deviceApi, IotDeviceMessageService messageService, IotModbusFrameEncoder frameEncoder, - IotModbusTcpSlaveConnectionManager connectionManager, - IotModbusTcpSlaveConfigCacheService configCacheService, - IotModbusTcpSlavePendingRequestManager pendingRequestManager, - IotModbusTcpSlavePollScheduler pollScheduler, + IotModbusTcpServerConnectionManager connectionManager, + IotModbusTcpServerConfigCacheService configCacheService, + IotModbusTcpServerPendingRequestManager pendingRequestManager, + IotModbusTcpServerPollScheduler pollScheduler, IotDeviceService deviceService, String serverId) { this.deviceApi = deviceApi; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java similarity index 95% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java index e659a89864..19ba6e900b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; @@ -18,13 +18,13 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** - * IoT Modbus TCP Slave 配置缓存:认证时按需加载,断连时清理,定时刷新已连接设备 + * IoT Modbus TCP Server 配置缓存:认证时按需加载,断连时清理,定时刷新已连接设备 * * @author 芋道源码 */ @RequiredArgsConstructor @Slf4j -public class IotModbusTcpSlaveConfigCacheService { +public class IotModbusTcpServerConfigCacheService { private final IotDeviceCommonApi deviceApi; @@ -45,7 +45,7 @@ public class IotModbusTcpSlaveConfigCacheService { IotModbusDeviceConfigListReqDTO reqDTO = new IotModbusDeviceConfigListReqDTO() .setStatus(CommonStatusEnum.ENABLE.getStatus()) .setMode(IotModbusModeEnum.POLLING.getMode()) - .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SLAVE.getType()) + .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SERVER.getType()) .setDeviceIds(Collections.singleton(deviceId)); CommonResult> result = deviceApi.getModbusDeviceConfigList(reqDTO); result.checkError(); @@ -81,7 +81,7 @@ public class IotModbusTcpSlaveConfigCacheService { CommonResult> result = deviceApi.getModbusDeviceConfigList( new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus()) .setMode(IotModbusModeEnum.POLLING.getMode()) - .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SLAVE.getType()) + .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SERVER.getType()) .setDeviceIds(connectedDeviceIds)); List modbusConfigs = result.getCheckedData(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java similarity index 98% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java index a0a7ad2949..781d8ac549 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import io.vertx.core.Future; @@ -15,14 +15,14 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** - * IoT Modbus TCP Slave 连接管理器 + * IoT Modbus TCP Server 连接管理器 *

                              * 管理设备 TCP 连接:socket ↔ 设备双向映射 * * @author 芋道源码 */ @Slf4j -public class IotModbusTcpSlaveConnectionManager { +public class IotModbusTcpServerConnectionManager { /** * socket → 连接信息 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPendingRequestManager.java similarity index 95% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPendingRequestManager.java index cea8f9862c..9bac7f86d5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPendingRequestManager.java @@ -1,9 +1,9 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -15,7 +15,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; /** - * IoT Modbus TCP Slave 待响应请求管理器 + * IoT Modbus TCP Server 待响应请求管理器 *

                              * 管理轮询下发的请求,用于匹配设备响应: * - TCP 模式:按 transactionId 精确匹配 @@ -24,7 +24,7 @@ import java.util.concurrent.ConcurrentLinkedDeque; * @author 芋道源码 */ @Slf4j -public class IotModbusTcpSlavePendingRequestManager { +public class IotModbusTcpServerPendingRequestManager { /** * deviceId → 有序队列 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPollScheduler.java similarity index 78% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPollScheduler.java index 31bb8d5fda..4660f8e7e7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPollScheduler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; @@ -7,9 +7,9 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager.PendingRequest; import io.vertx.core.Vertx; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -17,17 +17,17 @@ import lombok.extern.slf4j.Slf4j; import java.util.concurrent.atomic.AtomicInteger; /** - * IoT Modbus TCP Slave 轮询调度器:编码读请求帧,通过 TCP 连接发送到设备,注册 PendingRequest 等待响应 + * IoT Modbus TCP Server 轮询调度器:编码读请求帧,通过 TCP 连接发送到设备,注册 PendingRequest 等待响应 * * @author 芋道源码 */ @Slf4j -public class IotModbusTcpSlavePollScheduler extends AbstractIotModbusPollScheduler { +public class IotModbusTcpServerPollScheduler extends AbstractIotModbusPollScheduler { - private final IotModbusTcpSlaveConnectionManager connectionManager; + private final IotModbusTcpServerConnectionManager connectionManager; private final IotModbusFrameEncoder frameEncoder; - private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; - private final IotModbusTcpSlaveConfigCacheService configCacheService; + private final IotModbusTcpServerPendingRequestManager pendingRequestManager; + private final IotModbusTcpServerConfigCacheService configCacheService; private final int requestTimeout; /** * TCP 事务 ID 自增器(与 DownstreamHandler 共享) @@ -35,13 +35,13 @@ public class IotModbusTcpSlavePollScheduler extends AbstractIotModbusPollSchedul @Getter private final AtomicInteger transactionIdCounter; - public IotModbusTcpSlavePollScheduler(Vertx vertx, - IotModbusTcpSlaveConnectionManager connectionManager, + public IotModbusTcpServerPollScheduler(Vertx vertx, + IotModbusTcpServerConnectionManager connectionManager, IotModbusFrameEncoder frameEncoder, - IotModbusTcpSlavePendingRequestManager pendingRequestManager, + IotModbusTcpServerPendingRequestManager pendingRequestManager, int requestTimeout, AtomicInteger transactionIdCounter, - IotModbusTcpSlaveConfigCacheService configCacheService) { + IotModbusTcpServerConfigCacheService configCacheService) { super(vertx); this.connectionManager = connectionManager; this.frameEncoder = frameEncoder; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/package-info.java similarity index 56% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/package-info.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/package-info.java index cd8f1cb322..c15087027d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/package-info.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/package-info.java @@ -1,6 +1,6 @@ /** - * Modbus TCP Slave(从站)协议:设备主动连接网关,自定义 FC65 认证后由网关云端轮询 + * Modbus TCP Server(从站)协议:设备主动连接网关,自定义 FC65 认证后由网关云端轮询 *

                              * TCP Server 模式,支持 MODBUS_TCP / MODBUS_RTU 帧格式自动检测 */ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java index b3ae6a0ca4..933d56ade7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; @@ -33,9 +35,17 @@ public class IotTcpDownstreamHandler { */ public void handle(IotDeviceMessage message) { try { + // 1.1 检查是否是属性设置消息 + if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) { + return; + } + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { + log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod()); + return; + } log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", message.getDeviceId(), message.getMethod(), message.getId()); - // 1. 检查设备连接 + // 1.2 检查设备连接 IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( message.getDeviceId()); if (connectionInfo == null) { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index f5c77dbea0..71ee6f3361 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -164,22 +164,22 @@ yudao: trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 # ==================================== - # 针对引入的 Modbus TCP Master 组件的配置 + # 针对引入的 Modbus TCP Client 组件的配置 # ==================================== - - id: modbus-tcp-master-1 + - id: modbus-tcp-client-1 enabled: false - protocol: modbus_tcp_master + protocol: modbus_tcp_client port: 502 - modbus-tcp-master: + modbus-tcp-client: config-refresh-interval: 30 # 配置刷新间隔(秒) # ==================================== - # 针对引入的 Modbus TCP Slave 组件的配置 + # 针对引入的 Modbus TCP Server 组件的配置 # ==================================== - - id: modbus-tcp-slave-1 + - id: modbus-tcp-server-1 enabled: false - protocol: modbus_tcp_slave + protocol: modbus_tcp_server port: 503 - modbus-tcp-slave: + modbus-tcp-server: config-refresh-interval: 30 # 配置刷新间隔(秒) custom-function-code: 65 # 自定义功能码(用于认证等扩展交互) request-timeout: 5000 # Pending Request 超时时间(毫秒) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IoTModbusTcpMasterIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IoTModbusTcpClientIntegrationTest.java similarity index 94% rename from yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IoTModbusTcpMasterIntegrationTest.java rename to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IoTModbusTcpClientIntegrationTest.java index 40c8c5cdeb..62724b2f42 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IoTModbusTcpMasterIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IoTModbusTcpClientIntegrationTest.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient; import com.ghgande.j2mod.modbus.procimg.*; import com.ghgande.j2mod.modbus.slave.ModbusSlave; @@ -10,13 +10,13 @@ import org.junit.jupiter.api.Test; /** * Modbus TCP 从站模拟器(手动测试) * - *

                              测试场景:模拟一个标准 Modbus TCP 从站设备,供 Modbus TCP Master 网关连接和读写数据 + *

                              测试场景:模拟一个标准 Modbus TCP 从站设备,供 Modbus TCP Client 网关连接和读写数据 * *

                              使用步骤: *

                                *
                              1. 运行 {@link #testStartSlaveSimulator()} 启动模拟从站(默认端口 5020,从站地址 1)
                              2. - *
                              3. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-master 协议)
                              4. - *
                              5. 确保数据库有对应的 Modbus Master 设备配置(ip=127.0.0.1, port=5020, slaveId=1)
                              6. + *
                              7. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-client 协议)
                              8. + *
                              9. 确保数据库有对应的 Modbus Client 设备配置(ip=127.0.0.1, port=5020, slaveId=1)
                              10. *
                              11. 网关会自动连接模拟从站并开始轮询读取寄存器数据
                              12. *
                              13. 模拟器每 5 秒自动更新输入寄存器和保持寄存器的值,模拟传感器数据变化
                              14. *
                              @@ -33,7 +33,7 @@ import org.junit.jupiter.api.Test; */ @Slf4j @Disabled -public class IoTModbusTcpMasterIntegrationTest { +public class IoTModbusTcpClientIntegrationTest { private static final int PORT = 5020; private static final int SLAVE_ID = 1; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerRtuIntegrationTest.java similarity index 95% rename from yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java rename to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerRtuIntegrationTest.java index 041c8c3cf8..24029a19e6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerRtuIntegrationTest.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; import cn.hutool.core.util.HexUtil; import cn.hutool.json.JSONObject; @@ -7,9 +7,9 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetClient; @@ -28,13 +28,13 @@ import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; /** - * IoT Modbus TCP Slave 协议集成测试 — MODBUS_RTU 帧格式(手动测试) + * IoT Modbus TCP Server 协议集成测试 — MODBUS_RTU 帧格式(手动测试) * *

                              测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_RTU(CRC16)帧格式通信 * *

                              使用步骤: *

                                - *
                              1. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-slave 协议,默认端口 503)
                              2. + *
                              3. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-server 协议,默认端口 503)
                              4. *
                              5. 确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_rtu)
                              6. *
                              7. 运行以下测试方法: *
                                  @@ -49,7 +49,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; */ @Slf4j @Disabled -public class IotModbusTcpSlaveRtuIntegrationTest { +public class IotModbusTcpServerRtuIntegrationTest { private static final String SERVER_HOST = "127.0.0.1"; private static final int SERVER_PORT = 503; @@ -68,8 +68,8 @@ public class IotModbusTcpSlaveRtuIntegrationTest { // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== - private static final String PRODUCT_KEY = "modbus_tcp_slave_product_demo"; - private static final String DEVICE_NAME = "modbus_tcp_slave_device_demo_rtu"; + private static final String PRODUCT_KEY = "modbus_tcp_server_product_demo"; + private static final String DEVICE_NAME = "modbus_tcp_server_device_demo_rtu"; private static final String DEVICE_SECRET = "af01c55eb8e3424bb23fc6c783936b2e"; @BeforeAll diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerTcpIntegrationTest.java similarity index 95% rename from yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java rename to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerTcpIntegrationTest.java index 6360254f83..d00da5fe87 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerTcpIntegrationTest.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; import cn.hutool.core.util.HexUtil; import cn.hutool.json.JSONObject; @@ -6,9 +6,9 @@ import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetClient; @@ -29,13 +29,13 @@ import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; /** - * IoT Modbus TCP Slave 协议集成测试 — MODBUS_TCP 帧格式(手动测试) + * IoT Modbus TCP Server 协议集成测试 — MODBUS_TCP 帧格式(手动测试) * *

                                  测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_TCP(MBAP 头)帧格式通信 * *

                                  使用步骤: *

                                    - *
                                  1. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-slave 协议,默认端口 503)
                                  2. + *
                                  3. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-server 协议,默认端口 503)
                                  4. *
                                  5. 确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_tcp)
                                  6. *
                                  7. 运行以下测试方法: *
                                      @@ -50,7 +50,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; */ @Slf4j @Disabled -public class IotModbusTcpSlaveTcpIntegrationTest { +public class IotModbusTcpServerTcpIntegrationTest { private static final String SERVER_HOST = "127.0.0.1"; private static final int SERVER_PORT = 503; @@ -69,8 +69,8 @@ public class IotModbusTcpSlaveTcpIntegrationTest { // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== - private static final String PRODUCT_KEY = "modbus_tcp_slave_product_demo"; - private static final String DEVICE_NAME = "modbus_tcp_slave_device_demo_tcp"; + private static final String PRODUCT_KEY = "modbus_tcp_server_product_demo"; + private static final String DEVICE_NAME = "modbus_tcp_server_device_demo_tcp"; private static final String DEVICE_SECRET = "8e4adeb3d25342ab88643421d3fba3f6"; @BeforeAll From df4849d17c360c153d3f0ab7027f149954732980 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 13 Feb 2026 22:17:21 +0800 Subject: [PATCH 47/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E5=B0=86=20IotDeviceControlSceneRuleAction=20=E9=87=8D?= =?UTF-8?q?=E5=90=8D=E4=B8=BA=20IotDevicePropertySetSceneRuleAction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...uleAction.java => IotDevicePropertySetSceneRuleAction.java} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/{IotDeviceControlSceneRuleAction.java => IotDevicePropertySetSceneRuleAction.java} (98%) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDevicePropertySetSceneRuleAction.java similarity index 98% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDevicePropertySetSceneRuleAction.java index 79da298442..746e18d923 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java +++ b/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; import org.springframework.stereotype.Component; import java.util.List; -import java.util.Map; /** * IoT 设备属性设置的 {@link IotSceneRuleAction} 实现类 @@ -24,7 +23,7 @@ import java.util.Map; */ @Component @Slf4j -public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction { +public class IotDevicePropertySetSceneRuleAction implements IotSceneRuleAction { @Resource private IotDeviceService deviceService; From f64d84bb0c8186b0adb6f3e1e6369f4eb0fdd256 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 13 Feb 2026 23:39:14 +0800 Subject: [PATCH 48/53] =?UTF-8?q?feat=EF=BC=88iot=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E5=B0=86=20IotDeviceMessage=20=E4=B8=AD=E7=9A=84=20Map=20param?= =?UTF-8?q?s=20=E6=8A=BD=E6=88=90=20Topic=20DTO=20=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 IotDeviceStateUpdateReqDTO、IotDeviceOtaUpgradeReqDTO、IotDeviceOtaProgressReqDTO, 替换 IotDeviceMessage 和 IotOtaTaskRecordServiceImpl 中的裸 Map 构造和解析, 删除 IotDeviceMessage.buildOtaUpgrade() 方法。 Co-Authored-By: Claude Opus 4.6 --- .../ota/IotOtaTaskRecordServiceImpl.java | 23 +++++----- .../iot/core/mq/message/IotDeviceMessage.java | 14 ++---- .../topic/ota/IotDeviceOtaProgressReqDTO.java | 40 +++++++++++++++++ .../topic/ota/IotDeviceOtaUpgradeReqDTO.java | 45 +++++++++++++++++++ .../state/IotDeviceStateUpdateReqDTO.java | 24 ++++++++++ 5 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordServiceImpl.java index eb75b91540..f9cd776210 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordServiceImpl.java +++ b/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; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.lang.Assert; -import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record.IotOtaTaskRecordPageReqVO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.ota.IotDeviceOtaProgressReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.ota.IotDeviceOtaUpgradeReqDTO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; @@ -133,9 +137,9 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService { public boolean pushOtaTaskRecord(IotOtaTaskRecordDO record, IotOtaFirmwareDO fireware, IotDeviceDO device) { try { // 1. 推送 OTA 任务记录 - IotDeviceMessage message = IotDeviceMessage.buildOtaUpgrade( - fireware.getVersion(), fireware.getFileUrl(), fireware.getFileSize(), - fireware.getFileDigestAlgorithm(), fireware.getFileDigestValue()); + IotDeviceOtaUpgradeReqDTO params = BeanUtils.toBean(fireware, IotDeviceOtaUpgradeReqDTO.class); + IotDeviceMessage message = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), params); deviceMessageService.sendDeviceMessage(message, device); // 2. 更新 OTA 升级记录状态为进行中 @@ -163,17 +167,16 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService { @Override @Transactional(rollbackFor = Exception.class) - @SuppressWarnings("unchecked") public void updateOtaRecordProgress(IotDeviceDO device, IotDeviceMessage message) { // 1.1 参数解析 - Map params = (Map) message.getParams(); - String version = MapUtil.getStr(params, "version"); + IotDeviceOtaProgressReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceOtaProgressReqDTO.class); + String version = params.getVersion(); Assert.notBlank(version, "version 不能为空"); - Integer status = MapUtil.getInt(params, "status"); + Integer status = params.getStatus(); Assert.notNull(status, "status 不能为空"); Assert.notNull(IotOtaTaskRecordStatusEnum.of(status), "status 状态不正确"); - String description = MapUtil.getStr(params, "description"); - Integer progress = MapUtil.getInt(params, "progress"); + String description = params.getDescription(); + Integer progress = params.getProgress(); Assert.notNull(progress, "progress 不能为空"); Assert.isTrue(progress >= 0 && progress <= 100, "progress 必须在 0-100 之间"); // 1.2 查询 OTA 升级记录 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java index 830b5a0006..3b3e5c5d52 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java @@ -1,9 +1,9 @@ package cn.iocoder.yudao.module.iot.core.mq.message; -import cn.hutool.core.map.MapUtil; import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.topic.state.IotDeviceStateUpdateReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import lombok.AllArgsConstructor; import lombok.Builder; @@ -149,20 +149,12 @@ public class IotDeviceMessage { public static IotDeviceMessage buildStateUpdateOnline() { return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(), - MapUtil.of("state", IotDeviceStateEnum.ONLINE.getState())); + new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.ONLINE.getState())); } public static IotDeviceMessage buildStateOffline() { return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(), - MapUtil.of("state", IotDeviceStateEnum.OFFLINE.getState())); - } - - public static IotDeviceMessage buildOtaUpgrade(String version, String fileUrl, Long fileSize, - String fileDigestAlgorithm, String fileDigestValue) { - return requestOf(IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), MapUtil.builder() - .put("version", version).put("fileUrl", fileUrl).put("fileSize", fileSize) - .put("fileDigestAlgorithm", fileDigestAlgorithm).put("fileDigestValue", fileDigestValue) - .build()); + new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.OFFLINE.getState())); } } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java new file mode 100644 index 0000000000..84696c0651 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.core.topic.ota; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备 OTA 升级进度上报 Request DTO + *

                                      + * 用于 thing.ota.progress 上行消息的 params 参数 + * + * @author 芋道源码 + * @see 阿里云 - OTA 升级 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceOtaProgressReqDTO { + + /** + * 固件版本号 + */ + private String version; + + /** + * 升级状态 + */ + private Integer status; + + /** + * 描述信息 + */ + private String description; + + /** + * 升级进度(0-100) + */ + private Integer progress; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java new file mode 100644 index 0000000000..6832a46797 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.iot.core.topic.ota; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备 OTA 固件升级推送 Request DTO + *

                                      + * 用于 thing.ota.upgrade 下行消息的 params 参数 + * + * @author 芋道源码 + * @see 阿里云 - OTA 升级 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceOtaUpgradeReqDTO { + + /** + * 固件版本号 + */ + private String version; + + /** + * 固件文件下载地址 + */ + private String fileUrl; + + /** + * 固件文件大小(字节) + */ + private Long fileSize; + + /** + * 固件文件摘要算法 + */ + private String fileDigestAlgorithm; + + /** + * 固件文件摘要值 + */ + private String fileDigestValue; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java new file mode 100644 index 0000000000..a60095e192 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.iot.core.topic.state; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备状态更新 Request DTO + *

                                      + * 用于 thing.state.update 消息的 params 参数 + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceStateUpdateReqDTO { + + /** + * 设备状态 + */ + private Integer state; + +} From 5efb57838553737df2cba59c894ff333eafb8c3a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 14 Feb 2026 08:56:06 +0800 Subject: [PATCH 49/53] =?UTF-8?q?feat(iot):=20=E4=B8=BA=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=96=B9=E6=B3=95=E8=A1=A5=E9=BD=90=20Topic?= =?UTF-8?q?=20DTO=EF=BC=8C=E6=B6=88=E9=99=A4=E9=80=9A=E7=94=A8=20Map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增 3 个 DTO: - IotDevicePropertySetReqDTO:属性设置(下行) - IotDeviceServiceInvokeReqDTO:服务调用(下行) - IotDeviceConfigPushReqDTO:配置推送(下行) 2. 所有现有 DTO 的 javadoc 补充 {@link IotDeviceMessageMethodEnum#XXX} 引用 --- .../module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java | 5 ++++- .../module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java | 3 ++- .../iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java | 5 +++-- .../iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java | 3 ++- .../iot/core/topic/event/IotDeviceEventPostReqDTO.java | 3 ++- .../iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java | 3 ++- .../module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java | 3 ++- .../core/topic/property/IotDevicePropertyPackPostReqDTO.java | 3 ++- .../iot/core/topic/property/IotDevicePropertyPostReqDTO.java | 4 +++- .../iot/core/topic/state/IotDeviceStateUpdateReqDTO.java | 3 ++- .../module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java | 3 ++- .../iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java | 3 ++- .../iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java | 3 ++- .../module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java | 3 ++- .../module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java | 3 ++- 15 files changed, 34 insertions(+), 16 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java index a77cd428ad..ad938749d3 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java @@ -1,12 +1,15 @@ package cn.iocoder.yudao.module.iot.core.topic.auth; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import jakarta.validation.constraints.NotEmpty; import lombok.Data; /** * IoT 设备动态注册 Request DTO *

                                      - * 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret + * 用于 {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} 消息的 params 参数 + *

                                      + * 直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret * * @author 芋道源码 * @see 阿里云 - 一型一密 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java index 707f79890b..681aa72c5c 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.auth; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; /** * IoT 设备动态注册 Response DTO *

                                      - * 用于直连设备/网关的一型一密动态注册响应 + * 用于 {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} 响应的设备信息 * * @author 芋道源码 * @see 阿里云 - 一型一密 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java index cf34a1db2b..e2372e0cb8 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java @@ -1,13 +1,14 @@ package cn.iocoder.yudao.module.iot.core.topic.auth; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import jakarta.validation.constraints.NotEmpty; import lombok.Data; /** * IoT 子设备动态注册 Request DTO *

                                      - * 用于 thing.auth.register.sub 消息的 params 数组元素 - * + * 用于 {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} 消息的 params 数组元素 + *

                                      * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。 * * @author 芋道源码 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java index a45f14defe..7da2f4e47b 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.auth; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; /** * IoT 子设备动态注册 Response DTO *

                                      - * 用于 thing.auth.register.sub 响应的设备信息 + * 用于 {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} 响应的设备信息 * * @author 芋道源码 * @see 阿里云 - 动态注册子设备 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java index 3b6a7a7d4c..345419231c 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java @@ -1,11 +1,12 @@ package cn.iocoder.yudao.module.iot.core.topic.event; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import lombok.Data; /** * IoT 设备事件上报 Request DTO *

                                      - * 用于 thing.event.post 消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#EVENT_POST} 消息的 params 参数 * * @author 芋道源码 * @see 阿里云 - 设备上报事件 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java index 84696c0651..ef16e3e036 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.ota; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; /** * IoT 设备 OTA 升级进度上报 Request DTO *

                                      - * 用于 thing.ota.progress 上行消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#OTA_PROGRESS} 上行消息的 params 参数 * * @author 芋道源码 * @see 阿里云 - OTA 升级 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java index 6832a46797..096ac699b8 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.ota; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; /** * IoT 设备 OTA 固件升级推送 Request DTO *

                                      - * 用于 thing.ota.upgrade 下行消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#OTA_UPGRADE} 下行消息的 params 参数 * * @author 芋道源码 * @see 阿里云 - OTA 升级 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java index 24494984eb..509e457752 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.property; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import lombok.Data; @@ -9,7 +10,7 @@ import java.util.Map; /** * IoT 设备属性批量上报 Request DTO *

                                      - * 用于 thing.event.property.pack.post 消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_PACK_POST} 消息的 params 参数 * * @author 芋道源码 * @see 阿里云 - 网关批量上报数据 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java index 2e537442d7..98471d1d50 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java @@ -1,12 +1,14 @@ package cn.iocoder.yudao.module.iot.core.topic.property; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; + import java.util.HashMap; import java.util.Map; /** * IoT 设备属性上报 Request DTO *

                                      - * 用于 thing.property.post 消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_POST} 消息的 params 参数 *

                                      * 本质是一个 Map,key 为属性标识符,value 为属性值 * diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java index a60095e192..fce44e03b5 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.state; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; /** * IoT 设备状态更新 Request DTO *

                                      - * 用于 thing.state.update 消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#STATE_UPDATE} 消息的 params 参数 * * @author 芋道源码 */ diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java index 97ec33200a..b9444ed6d6 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.iot.core.topic.topo; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import jakarta.validation.constraints.NotEmpty; import lombok.Data; @@ -9,7 +10,7 @@ import java.util.List; /** * IoT 设备拓扑添加 Request DTO *

                                      - * 用于 thing.topo.add 消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_ADD} 消息的 params 参数 * * @author 芋道源码 * @see 阿里云 - 添加拓扑关系 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java index 0198206fe3..615e509ae6 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.topo; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import lombok.AllArgsConstructor; import lombok.Data; @@ -10,7 +11,7 @@ import java.util.List; /** * IoT 设备拓扑关系变更通知 Request DTO *

                                      - * 用于 thing.topo.change 下行消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_CHANGE} 下行消息的 params 参数 * * @author 芋道源码 * @see 阿里云 - 通知网关拓扑关系变化 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java index 71ee2bb8b2..6db2b5db8d 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.topo; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; @@ -10,7 +11,7 @@ import java.util.List; /** * IoT 设备拓扑删除 Request DTO *

                                      - * 用于 thing.topo.delete 消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_DELETE} 消息的 params 参数 * * @author 芋道源码 * @see 阿里云 - 删除拓扑关系 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java index 7a61af0a58..1da86c9505 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java @@ -1,11 +1,12 @@ package cn.iocoder.yudao.module.iot.core.topic.topo; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import lombok.Data; /** * IoT 设备拓扑关系获取 Request DTO *

                                      - * 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展) + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_GET} 请求的 params 参数(目前为空,预留扩展) * * @author 芋道源码 * @see 阿里云 - 获取拓扑关系 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java index 69c9b1555e..0aef9c8680 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.topo; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import lombok.Data; @@ -8,7 +9,7 @@ import java.util.List; /** * IoT 设备拓扑关系获取 Response DTO *

                                      - * 用于 thing.topo.get 响应 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_GET} 响应 * * @author 芋道源码 * @see 阿里云 - 获取拓扑关系 From 5083dab10b9115b99993905b62129b5b7a20cbe9 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 14 Feb 2026 08:56:14 +0800 Subject: [PATCH 50/53] =?UTF-8?q?feat(iot):=20=E4=B8=BA=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=96=B9=E6=B3=95=E8=A1=A5=E9=BD=90=20Topic?= =?UTF-8?q?=20DTO=EF=BC=8C=E6=B6=88=E9=99=A4=E9=80=9A=E7=94=A8=20Map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增 3 个 DTO: - IotDevicePropertySetReqDTO:属性设置(下行) - IotDeviceServiceInvokeReqDTO:服务调用(下行) - IotDeviceConfigPushReqDTO:配置推送(下行) 2. 所有现有 DTO 的 javadoc 补充 {@link IotDeviceMessageMethodEnum#XXX} 引用 --- .../config/IotDeviceConfigPushReqDTO.java | 54 +++++++++++++++++++ .../property/IotDevicePropertySetReqDTO.java | 37 +++++++++++++ .../service/IotDeviceServiceInvokeReqDTO.java | 32 +++++++++++ 3 files changed, 123 insertions(+) create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/config/IotDeviceConfigPushReqDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertySetReqDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/service/IotDeviceServiceInvokeReqDTO.java diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/config/IotDeviceConfigPushReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/config/IotDeviceConfigPushReqDTO.java new file mode 100644 index 0000000000..4828c9917a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/config/IotDeviceConfigPushReqDTO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.core.topic.config; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备配置推送 Request DTO + *

                                      + * 用于 {@link IotDeviceMessageMethodEnum#CONFIG_PUSH} 下行消息的 params 参数 + * + * @author 芋道源码 + * @see 阿里云 - 远程配置 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceConfigPushReqDTO { + + /** + * 配置编号 + */ + private String configId; + + /** + * 配置文件大小(字节) + */ + private Long configSize; + + /** + * 签名方法 + */ + private String signMethod; + + /** + * 签名 + */ + private String sign; + + /** + * 配置文件下载地址 + */ + private String url; + + /** + * 获取类型 + *

                                      + * file: 文件 + * content: 内容 + */ + private String getType; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertySetReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertySetReqDTO.java new file mode 100644 index 0000000000..ba51f1bba1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertySetReqDTO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.core.topic.property; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; + +import java.util.HashMap; +import java.util.Map; + +/** + * IoT 设备属性设置 Request DTO + *

                                      + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_SET} 下行消息的 params 参数 + *

                                      + * 本质是一个 Map,key 为属性标识符,value 为属性值 + * + * @author 芋道源码 + */ +public class IotDevicePropertySetReqDTO extends HashMap { + + public IotDevicePropertySetReqDTO() { + super(); + } + + public IotDevicePropertySetReqDTO(Map properties) { + super(properties); + } + + /** + * 创建属性设置 DTO + * + * @param properties 属性数据 + * @return DTO 对象 + */ + public static IotDevicePropertySetReqDTO of(Map properties) { + return new IotDevicePropertySetReqDTO(properties); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/service/IotDeviceServiceInvokeReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/service/IotDeviceServiceInvokeReqDTO.java new file mode 100644 index 0000000000..dafadd24a2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/service/IotDeviceServiceInvokeReqDTO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.iot.core.topic.service; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * IoT 设备服务调用 Request DTO + *

                                      + * 用于 {@link IotDeviceMessageMethodEnum#SERVICE_INVOKE} 下行消息的 params 参数 + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceServiceInvokeReqDTO { + + /** + * 服务标识符 + */ + private String identifier; + + /** + * 服务输入参数 + */ + private Map inputParams; + +} From cc9cca82cb25240b9ce174632c447a61a075cf61 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 14 Feb 2026 09:31:51 +0800 Subject: [PATCH 51/53] =?UTF-8?q?fix(iot):=20=E4=BF=AE=E5=A4=8D=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E5=92=8C=E6=9E=9A=E4=B8=BE=E6=8F=8F=E8=BF=B0=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IotDeviceMessage/IotDeviceMessageDO/IotDeviceMessageRespVO: thing.property.report→thing.property.post - IotDeviceMessageMethodEnum: OTA 固定信息推送→OTA 固件信息推送 --- .../admin/device/vo/message/IotDeviceMessageRespVO.java | 2 +- .../module/iot/dal/dataobject/device/IotDeviceMessageDO.java | 2 +- .../yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java | 2 +- .../yudao/module/iot/core/mq/message/IotDeviceMessage.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespVO.java index e53f5acb60..f9e4b75290 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespVO.java +++ b/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 { @Schema(description = "请求编号", example = "req_123") private String requestId; - @Schema(description = "请求方法", requiredMode = Schema.RequiredMode.REQUIRED, example = "thing.property.report") + @Schema(description = "请求方法", requiredMode = Schema.RequiredMode.REQUIRED, example = "thing.property.post") private String method; @Schema(description = "请求参数") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java index 9f1f6a6a0c..233b2c1402 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java +++ b/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 { * 请求方法 * * 枚举 {@link IotDeviceMessageMethodEnum} - * 例如说:thing.property.report 属性上报 + * 例如说:thing.property.post 属性上报 */ private String method; /** diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java index d980032842..3b4495e333 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java +++ b/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 { // ========== OTA 固件 ========== // 可参考:https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates - OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false), + OTA_UPGRADE("thing.ota.upgrade", "OTA 固件信息推送", false), OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true), ; diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java index 3b3e5c5d52..cc9b138744 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java @@ -72,7 +72,7 @@ public class IotDeviceMessage { * 请求方法 * * 枚举 {@link IotDeviceMessageMethodEnum} - * 例如说:thing.property.report 属性上报 + * 例如说:thing.property.post 属性上报 */ private String method; /** From 9240e16c396b0e43347af3fe35b0804084eefcc6 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 14 Feb 2026 16:02:33 +0800 Subject: [PATCH 52/53] =?UTF-8?q?feat=EF=BC=9A=E5=90=8C=E6=AD=A5=E6=9C=80?= =?UTF-8?q?=E6=96=B0=20mysql=20=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/ruoyi-vue-pro.sql | 211 +++++++++++++++++++++++++----------- 1 file changed, 147 insertions(+), 64 deletions(-) diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index 526f2c17ad..3e4ca9ef30 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -11,7 +11,7 @@ Target Server Version : 80200 (8.2.0) File Encoding : 65001 - Date: 26/11/2025 22:43:12 + Date: 14/02/2026 16:02:08 */ SET NAMES utf8mb4; @@ -91,7 +91,7 @@ CREATE TABLE `infra_api_error_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 23210 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志'; +) ENGINE = InnoDB AUTO_INCREMENT = 23367 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志'; -- ---------------------------- -- Records of infra_api_error_log @@ -128,7 +128,7 @@ CREATE TABLE `infra_codegen_column` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2659 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义'; +) ENGINE = InnoDB AUTO_INCREMENT = 2880 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义'; -- ---------------------------- -- Records of infra_codegen_column @@ -166,7 +166,7 @@ CREATE TABLE `infra_codegen_table` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 197 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义'; +) ENGINE = InnoDB AUTO_INCREMENT = 210 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义'; -- ---------------------------- -- Records of infra_codegen_table @@ -204,7 +204,6 @@ INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `val INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 'url', 2, 'SkyWalking 监控的地址', 'url.skywalking', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:57:03', b'0'); INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (9, 'url', 2, 'Spring Boot Admin 监控的地址', 'url.spring-boot-admin', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:52:07', b'0'); INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (10, 'url', 2, 'Swagger 接口文档的地址', 'url.swagger', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:59:00', b'0'); -INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (11, 'ui', 2, '腾讯地图 key', 'tencent.lbs.key', 'TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E', b'1', '腾讯地图 key', '1', '2023-06-03 19:16:27', '1', '2023-06-03 19:16:27', b'0'); INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (12, 'test2', 2, 'test3', 'test4', 'test5', b'1', 'test6', '1', '2023-12-03 09:55:16', '1', '2025-04-06 21:00:09', b'0'); INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (13, '用户管理-账号初始密码', 2, '用户管理-注册开关', 'system.user.register-enabled', 'true', b'0', '', '1', '2025-04-26 17:23:41', '1', '2025-04-26 17:23:41', b'0'); COMMIT; @@ -225,7 +224,7 @@ CREATE TABLE `infra_data_source_config` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '数据源配置表'; +) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '数据源配置表'; -- ---------------------------- -- Records of infra_data_source_config @@ -251,7 +250,7 @@ CREATE TABLE `infra_file` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2142 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; +) ENGINE = InnoDB AUTO_INCREMENT = 2163 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; -- ---------------------------- -- Records of infra_file @@ -292,7 +291,7 @@ INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `c INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (29, '本地存储(示例)', 10, 'mac/linux 使用 /,windows 使用 \\', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClientConfig\",\"basePath\":\"/Users/yunai/tmp/file\",\"domain\":\"http://127.0.0.1:48080\"}', '1', '2025-05-02 11:25:45', '1', '2025-11-24 20:57:14', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (30, 'SFTP 存储(示例)', 12, '', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileClientConfig\",\"basePath\":\"/upload\",\"domain\":\"http://127.0.0.1:48080\",\"host\":\"127.0.0.1\",\"port\":2222,\"username\":\"foo\",\"password\":\"pass\"}', '1', '2025-05-02 16:34:10', '1', '2025-11-24 20:57:14', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (34, '七牛云存储【私有】(示例)', 20, '请换成你自己的密钥!!!', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"s3.cn-south-1.qiniucs.com\",\"domain\":\"http://t151glocd.hn-bkt.clouddn.com\",\"bucket\":\"ruoyi-vue-pro-private\",\"accessKey\":\"3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS\",\"accessSecret\":\"wd0tbVBYlp0S-ihA8Qg2hPLncoP83wyrIq24OZuY\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":false}', '1', '2025-08-17 21:22:00', '1', '2025-11-24 20:57:14', b'0'); -INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (35, '1', 20, '1', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"http://www.baidu.com\",\"domain\":\"http://www.xxx.com\",\"bucket\":\"1\",\"accessKey\":\"2\",\"accessSecret\":\"3\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":false}', '1', '2025-10-02 14:32:12', '1', '2025-11-24 20:57:14', b'0'); +INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (35, '1', 20, '1', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"http://www.baidu.com\",\"domain\":\"http://www.xxx.com\",\"bucket\":\"1\",\"accessKey\":\"2\",\"accessSecret\":\"3\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":false,\"region\":\"1\"}', '1', '2025-10-02 14:32:12', '1', '2025-11-29 15:59:39', b'0'); COMMIT; -- ---------------------------- @@ -413,16 +412,16 @@ CREATE TABLE `system_dept` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 116 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '部门表'; +) ENGINE = InnoDB AUTO_INCREMENT = 118 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '部门表'; -- ---------------------------- -- Records of system_dept -- ---------------------------- BEGIN; -INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, '芋道源码', 0, 0, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2025-03-29 15:47:53', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, '芋道源码', 0, 0, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2026-01-04 18:01:12', b'0', 1); INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '深圳总公司', 100, 1, 104, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2025-03-29 15:49:55', b'0', 1); INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (102, '长沙分公司', 100, 2, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:40', b'0', 1); -INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, '研发部门', 101, 1, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2024-10-02 10:22:03', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, '研发部门', 101, 1, 104, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2026-01-04 18:01:24', b'0', 1); INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, '市场部门', 101, 2, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:38', b'0', 1); INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (105, '测试部门', 101, 3, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2022-05-16 20:25:15', b'0', 1); INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (106, '财务部门', 101, 4, 103, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '103', '2022-01-15 21:32:22', b'0', 1); @@ -433,6 +432,8 @@ INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, '顶级部门', 0, 1, NULL, NULL, NULL, 0, '113', '2022-03-07 21:44:50', '113', '2022-03-07 21:44:50', b'0', 122); INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (112, '产品部门', 101, 100, 1, NULL, NULL, 1, '1', '2023-12-02 09:45:13', '1', '2023-12-02 09:45:31', b'0', 1); INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, '支持部门', 102, 3, 104, NULL, NULL, 1, '1', '2023-12-02 09:47:38', '1', '2025-03-29 15:00:56', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (116, '某个子部门', 0, 1, NULL, NULL, NULL, 0, '1', '2025-12-08 14:51:12', '1', '2025-12-08 14:51:12', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, '某个子部门 2', 0, 2, NULL, NULL, NULL, 0, '1', '2025-12-08 14:51:25', '1', '2025-12-08 14:51:25', b'0', 1); COMMIT; -- ---------------------------- @@ -455,13 +456,13 @@ CREATE TABLE `system_dict_data` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 3035 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表'; +) ENGINE = InnoDB AUTO_INCREMENT = 3054 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表'; -- ---------------------------- -- Records of system_dict_data -- ---------------------------- BEGIN; -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 1, '男', '1', 'system_user_sex', 0, 'default', 'A', '性别男', 'admin', '2021-01-05 17:03:48', '1', '2022-03-29 00:14:39', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 1, '男', '1', 'system_user_sex', 0, 'primary', 'A', '性别男', 'admin', '2021-01-05 17:03:48', '1', '2025-12-10 13:19:26', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 2, '女', '2', 'system_user_sex', 0, 'success', '', '性别女', 'admin', '2021-01-05 17:03:48', '1', '2023-11-15 23:30:37', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 1, '正常', '1', 'infra_job_status', 0, 'success', '', '正常状态', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 19:33:38', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (9, 2, '暂停', '2', 'infra_job_status', 0, 'danger', '', '停用状态', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 19:33:45', b'0'); @@ -1060,7 +1061,6 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3000, 16, '百川智能', 'BaiChuan', 'ai_platform', 0, '', '', '', '1', '2025-03-23 12:15:46', '1', '2025-03-23 12:15:46', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3001, 40, 'Vben5.0 Ant Design Schema 模版', '40', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-04-23 21:47:47', '1', '2025-09-04 23:25:12', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3002, 6, '支付宝余额', '6', 'brokerage_withdraw_type', 0, '', '', 'API 打款', '1', '2025-05-10 08:24:49', '1', '2025-05-10 08:24:49', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3003, 1, 'Alink', 'Alink', 'iot_codec_type', 0, '', '', '阿里云 Alink', '1', '2025-06-12 22:56:06', '1', '2025-06-12 23:22:24', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3004, 3, 'WARN', '3', 'iot_alert_level', 0, 'warning', '', '', '1', '2025-06-27 20:32:22', '1', '2025-06-27 20:34:31', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3005, 1, 'INFO', '1', 'iot_alert_level', 0, 'primary', '', '', '1', '2025-06-27 20:33:28', '1', '2025-06-27 20:34:35', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3006, 5, 'ERROR', '5', 'iot_alert_level', 0, 'danger', '', '', '1', '2025-06-27 20:33:50', '1', '2025-06-27 20:33:50', b'0'); @@ -1078,9 +1078,6 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3018, 30, '升级成功', '30', 'iot_ota_task_record_status', 0, 'success', '', '', '1', '2025-07-02 09:45:47', '1', '2025-07-02 09:45:47', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3019, 40, '升级失败', '40', 'iot_ota_task_record_status', 0, 'danger', '', '', '1', '2025-07-02 09:46:02', '1', '2025-07-02 09:46:02', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3020, 50, '升级取消', '50', 'iot_ota_task_record_status', 0, 'warning', '', '', '1', '2025-07-02 09:46:09', '\"1\"', '2025-07-02 09:46:27', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3021, 1, 'IP 定位', '1', 'iot_location_type', 0, '', '', '', '1', '2025-07-05 09:56:46', '1', '2025-07-05 09:56:46', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3022, 2, '设备上报', '2', 'iot_location_type', 0, '', '', '', '1', '2025-07-05 09:56:57', '1', '2025-07-05 09:56:57', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3023, 3, '手动定位', '3', 'iot_location_type', 0, '', '', '', '1', '2025-07-05 09:57:05', '1', '2025-07-05 09:57:05', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3024, 3, '设备事件上报', '3', 'iot_rule_scene_trigger_type_enum', 0, '', '', '', '1', '2025-07-06 10:28:29', '1', '2025-07-06 10:28:29', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3025, 4, '设备服务调用', '4', 'iot_rule_scene_trigger_type_enum', 0, '', '', '', '1', '2025-07-06 10:28:35', '1', '2025-07-06 10:28:35', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3026, 100, '定时触发', '100', 'iot_rule_scene_trigger_type_enum', 0, '', '', '', '1', '2025-07-06 10:28:48', '1', '2025-07-06 10:28:48', b'0'); @@ -1093,6 +1090,21 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3033, 51, 'Vben5.0 Element Plus 标准模版', '51', 'infra_codegen_front_type', 0, '', '', '', '1', '2025-09-04 23:26:49', '1', '2025-09-04 23:26:49', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', '2026-02-04 00:32:47', '1', '2026-02-04 00:32:47', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3043, 4, 'MQTT', 'mqtt', 'iot_protocol_type', 0, 'success', '', 'MQTT 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3044, 5, 'EMQX', 'emqx', 'iot_protocol_type', 0, 'success', '', 'EMQX 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3045, 6, 'CoAP', 'coap', 'iot_protocol_type', 0, '', '', 'CoAP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3046, 7, 'Modbus TCP Server', 'modbus_tcp_server', 'iot_protocol_type', 0, '', '', 'Modbus TCP Server 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-12 15:16:45', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3047, 0, 'JSON', 'json', 'iot_serialize_type', 0, 'success', '', 'JSON 格式', '1', '2026-02-04 00:33:19', '1', '2026-02-04 00:33:19', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3048, 1, '二进制', 'binary', 'iot_serialize_type', 0, 'warning', '', '二进制格式', '1', '2026-02-04 00:33:19', '1', '2026-02-04 00:33:19', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3049, 8, 'Modbus TCP Client', 'modbus_tcp_client', 'iot_protocol_type', 0, '', '', 'Modbus TCP Client 协议', '1', '2026-02-08 18:29:46', '1', '2026-02-12 15:16:32', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3050, 2, '边缘采集', '2', 'iot_modbus_mode', 0, 'success', '', '设备主动上报数据,无需轮询', '1', '2025-06-12 22:56:06', '1', '2026-02-09 13:03:23', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3051, 1, 'Modbus TCP', '1', 'iot_modbus_frame_format', 0, 'default', '', 'MBAP 头部格式', '1', '2025-06-12 22:56:06', '1', '2025-06-12 22:56:06', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3052, 2, 'Modbus RTU', '2', 'iot_modbus_frame_format', 0, 'warning', '', 'CRC16 校验格式', '1', '2025-06-12 22:56:06', '1', '2025-06-12 22:56:06', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3053, 1, '云端轮询', '1', 'iot_modbus_mode', 0, 'primary', '', '网关主动轮询读取设备寄存器', '1', '2025-06-12 22:56:06', '1', '2025-06-12 22:56:06', b'0'); COMMIT; -- ---------------------------- @@ -1112,7 +1124,7 @@ CREATE TABLE `system_dict_type` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2008 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; +) ENGINE = InnoDB AUTO_INCREMENT = 2012 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; -- ---------------------------- -- Records of system_dict_type @@ -1221,14 +1233,16 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1014, 'IoT 场景流转的触发类型枚举', 'iot_rule_scene_trigger_type_enum', 0, '', '1', '2025-03-20 14:59:44', '1', '2025-03-20 14:59:44', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1015, 'IoT 设备消息类型枚举', 'iot_device_message_type_enum', 0, '', '1', '2025-03-20 15:01:15', '1', '2025-03-20 15:01:15', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1016, 'IoT 规则场景的触发类型枚举', 'iot_rule_scene_action_type_enum', 0, '', '1', '2025-03-28 15:26:54', '1', '2025-03-28 15:29:13', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2000, 'IoT 数据格式', 'iot_codec_type', 0, 'IoT 编解码器类型', '1', '2025-06-12 22:55:46', '1', '2025-06-12 22:55:46', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2001, 'IoT 告警级别', 'iot_alert_level', 0, '', '1', '2025-06-27 20:30:57', '1', '2025-06-27 20:30:57', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2002, 'IoT 告警', 'iot_alert_receive_type', 0, '', '1', '2025-06-27 22:49:19', '1', '2025-06-27 22:49:19', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2003, 'IoT 固件设备范围', 'iot_ota_task_device_scope', 0, '', '1', '2025-07-02 09:42:49', '1', '2025-07-02 09:42:49', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2004, 'IoT 固件升级任务状态', 'iot_ota_task_status', 0, '', '1', '2025-07-02 09:43:43', '1', '2025-07-02 09:43:43', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2005, 'IoT 固件升级记录状态', 'iot_ota_task_record_status', 0, '', '1', '2025-07-02 09:45:02', '1', '2025-07-02 09:45:02', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2006, 'IoT 定位类型', 'iot_location_type', 0, '', '1', '2025-07-05 09:56:25', '1', '2025-07-05 09:56:25', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2007, 'AI MCP 客户端名字', 'ai_mcp_client_name', 0, '', '1', '2025-08-28 13:57:40', '1', '2025-08-28 13:57:40', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2008, 'IoT 协议类型', 'iot_protocol_type', 0, 'IoT 设备接入协议类型', '1', '2026-02-04 00:31:33', '1', '2026-02-04 00:31:33', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2009, 'IoT 序列化类型', 'iot_serialize_type', 0, 'IoT 设备消息序列化类型', '1', '2026-02-04 00:33:16', '1', '2026-02-04 00:33:16', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2010, 'IoT Modbus 工作模式', 'iot_modbus_mode', 0, 'Modbus 设备数据采集模式', '1', '2025-06-12 22:55:46', '1', '2025-06-12 22:55:46', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2011, 'IoT Modbus 帧格式', 'iot_modbus_frame_format', 0, 'Modbus 数据帧协议格式', '1', '2025-06-12 22:55:46', '1', '2025-06-12 22:55:46', b'0', '1970-01-01 00:00:00'); COMMIT; -- ---------------------------- @@ -1252,7 +1266,7 @@ CREATE TABLE `system_login_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 4066 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录'; +) ENGINE = InnoDB AUTO_INCREMENT = 4449 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录'; -- ---------------------------- -- Records of system_login_log @@ -1286,7 +1300,7 @@ CREATE TABLE `system_mail_account` ( -- ---------------------------- BEGIN; INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `starttls_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '7684413@qq.com', '7684413@qq.com', '1234576', '127.0.0.1', 8080, b'0', b'0', '1', '2023-01-25 17:39:52', '1', '2025-04-04 16:34:40', b'0'); -INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `starttls_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'ydym_test@163.com', 'ydym_test@163.com', 'WBZTEINMIFVRYSOE', 'smtp.163.com', 465, b'1', b'0', '1', '2023-01-26 01:26:03', '1', '2025-07-26 21:57:55', b'0'); +INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `starttls_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'ydym_test@163.com', 'ydym_test@163.com', 'WBZTEINMIFVRYSOE', 'smtp.163.com', 465, b'1', b'0', '1', '2023-01-26 01:26:03', '1', '2025-12-20 18:09:32', b'0'); INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `starttls_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3, '76854114@qq.com', '3335', '11234', 'yunai1.cn', 466, b'0', b'0', '1', '2023-01-27 15:06:38', '1', '2023-01-27 07:08:36', b'1'); INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `starttls_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '7685413x@qq.com', '2', '3', '4', 5, b'1', b'0', '1', '2023-04-12 23:05:06', '1', '2023-04-12 15:05:11', b'1'); COMMIT; @@ -1385,7 +1399,7 @@ CREATE TABLE `system_menu` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 5047 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表'; +) ENGINE = InnoDB AUTO_INCREMENT = 5049 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表'; -- ---------------------------- -- Records of system_menu @@ -1394,8 +1408,8 @@ BEGIN; INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '系统管理', '', 1, 10, 0, '/system', 'ep:tools', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2025-03-15 21:30:27', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, '基础设施', '', 1, 20, 0, '/infra', 'ep:monitor', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-03-01 08:28:40', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5, 'OA 示例', '', 1, 40, 1185, 'oa', 'fa:road', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-09-20 16:26:19', '1', '2024-02-29 12:38:13', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (100, '用户管理', 'system:user:list', 2, 1, 1, 'user', 'ep:avatar', 'system/user/index', 'SystemUser', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2025-03-15 21:30:41', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (101, '角色管理', '', 2, 2, 1, 'role', 'ep:user', 'system/role/index', 'SystemRole', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-05-01 18:35:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (100, '用户管理', 'system:user:list', 2, 1, 1, 'user', 'ep:avatar', 'system/user/index', 'SystemUser', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2026-01-01 18:43:01', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (101, '角色管理', '', 2, 2, 1, 'role', 'ep:user', 'system/role/index', 'SystemRole', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2026-01-05 19:30:33', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (102, '菜单管理', '', 2, 3, 1, 'menu', 'ep:menu', 'system/menu/index', 'SystemMenu', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:03:50', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (103, '部门管理', '', 2, 4, 1, 'dept', 'fa:address-card', 'system/dept/index', 'SystemDept', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:28', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (104, '岗位管理', '', 2, 5, 1, 'post', 'fa:address-book-o', 'system/post/index', 'SystemPost', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:39', b'0'); @@ -1730,7 +1744,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2156, '查询项目', 'report:go-view-project:query', 3, 0, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:25:53', '1', '2023-02-07 19:25:53', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2157, '使用 SQL 查询数据', 'report:go-view-data:get-by-sql', 3, 3, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:26:15', '1', '2023-02-07 19:26:15', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2158, '使用 HTTP 查询数据', 'report:go-view-data:get-by-http', 3, 4, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:26:35', '1', '2023-02-07 19:26:35', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2159, 'Boot 开发文档', '', 1, 1, 0, 'https://doc.iocoder.cn/', 'ep:document', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-02-10 22:46:28', '1', '2024-07-28 11:36:48', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2159, 'Boot 开发文档', '', 1, 1, 0, 'https://doc.iocoder.cn/', 'ep:document', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-02-10 22:46:28', '1', '2026-01-05 19:31:07', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2160, 'Cloud 开发文档', '', 1, 2, 0, 'https://cloud.iocoder.cn', 'ep:document-copy', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-02-10 22:47:07', '1', '2023-12-02 21:32:29', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2161, '接入示例', '', 1, 99, 1117, 'demo', 'fa-solid:dragon', 'pay/demo/index', NULL, 0, b'1', b'1', b'1', '', '2023-02-11 14:21:42', '1', '2024-01-18 23:50:00', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2162, '商品导出', 'product:spu:export', 3, 5, 2014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-30 14:22:58', '', '2022-07-30 14:22:58', b'0'); @@ -2141,7 +2155,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2722, '流程实例的查询(管理员)', 'bpm:process-instance:manager-query', 3, 1, 2721, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-03-22 08:18:27', '1', '2024-03-22 08:19:05', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2723, '流程实例的取消(管理员)', 'bpm:process-instance:cancel-by-admin', 3, 2, 2721, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-03-22 08:19:25', '1', '2024-03-22 08:19:25', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2724, '流程任务', '', 2, 11, 1186, 'process-tasnk', 'ep:collection-tag', 'bpm/task/manager/index', 'BpmManagerTask', 0, b'1', b'1', b'1', '1', '2024-03-22 08:43:22', '1', '2024-03-22 08:43:27', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2725, '流程任务的查询(管理员)', 'bpm:task:mananger-query', 3, 1, 2724, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-03-22 08:43:49', '1', '2024-03-22 08:43:49', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2725, '流程任务的查询(管理员)', 'bpm:task:manager-query', 3, 1, 2724, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-03-22 08:43:49', '1', '2025-12-23 23:04:44', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2726, '流程监听器', '', 2, 5, 1186, 'process-listener', 'fa:assistive-listening-systems', 'bpm/processListener/index', 'BpmProcessListener', 0, b'1', b'1', b'1', '', '2024-03-09 16:05:34', '1', '2024-03-23 13:13:38', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2727, '流程监听器查询', 'bpm:process-listener:query', 3, 1, 2726, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-03-09 16:05:34', '', '2024-03-09 16:05:34', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2728, '流程监听器创建', 'bpm:process-listener:create', 3, 2, 2726, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-03-09 16:05:34', '', '2024-03-09 16:05:34', b'0'); @@ -2386,13 +2400,13 @@ CREATE TABLE `system_notify_message` ( -- Records of system_notify_message -- ---------------------------- BEGIN; -INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 1, 2, 1, 'test', '123', '我是 1,我开始 2 了', 1, '{\"name\":\"1\",\"what\":\"2\"}', b'1', '2025-04-21 14:59:37', '1', '2023-01-28 11:44:08', '1', '2025-04-21 14:59:37', b'0', 1); -INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, 1, 2, 1, 'test', '123', '我是 1,我开始 2 了', 1, '{\"name\":\"1\",\"what\":\"2\"}', b'1', '2025-04-21 14:59:37', '1', '2023-01-28 11:45:04', '1', '2025-04-21 14:59:37', b'0', 1); +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 1, 2, 1, 'test', '123', '我是 1,我开始 2 了', 1, '{\"name\":\"1\",\"what\":\"2\"}', b'1', '2025-12-15 21:24:36', '1', '2023-01-28 11:44:08', '1', '2025-12-15 21:24:36', b'0', 1); +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, 1, 2, 1, 'test', '123', '我是 1,我开始 2 了', 1, '{\"name\":\"1\",\"what\":\"2\"}', b'1', '2025-12-15 21:24:36', '1', '2023-01-28 11:45:04', '1', '2025-12-15 21:24:36', b'0', 1); INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, 103, 2, 2, 'register', '系统消息', '你好,欢迎 哈哈 加入大家庭!', 2, '{\"name\":\"哈哈\"}', b'0', NULL, '1', '2023-01-28 21:02:20', '1', '2023-01-28 21:02:20', b'0', 1); -INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5, 1, 2, 1, 'test', '123', '我是 芋艿,我开始 写代码 了', 1, '{\"name\":\"芋艿\",\"what\":\"写代码\"}', b'1', '2025-04-21 14:59:37', '1', '2023-01-28 22:21:42', '1', '2025-04-21 14:59:37', b'0', 1); -INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6, 1, 2, 1, 'test', '123', '我是 芋艿,我开始 写代码 了', 1, '{\"name\":\"芋艿\",\"what\":\"写代码\"}', b'1', '2025-04-21 14:59:36', '1', '2023-01-28 22:22:07', '1', '2025-04-21 14:59:36', b'0', 1); -INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (7, 1, 2, 1, 'test', '123', '我是 2,我开始 3 了', 1, '{\"name\":\"2\",\"what\":\"3\"}', b'1', '2025-04-21 14:59:35', '1', '2023-01-28 23:45:21', '1', '2025-04-21 14:59:35', b'0', 1); -INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (8, 1, 2, 2, 'register', '系统消息', '你好,欢迎 123 加入大家庭!', 2, '{\"name\":\"123\"}', b'1', '2025-04-21 14:59:35', '1', '2023-01-28 23:50:21', '1', '2025-04-21 14:59:35', b'0', 1); +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5, 1, 2, 1, 'test', '123', '我是 芋艿,我开始 写代码 了', 1, '{\"name\":\"芋艿\",\"what\":\"写代码\"}', b'1', '2025-12-08 17:25:28', '1', '2023-01-28 22:21:42', '1', '2025-12-08 17:25:28', b'0', 1); +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6, 1, 2, 1, 'test', '123', '我是 芋艿,我开始 写代码 了', 1, '{\"name\":\"芋艿\",\"what\":\"写代码\"}', b'1', '2025-12-08 17:25:30', '1', '2023-01-28 22:22:07', '1', '2025-12-08 17:25:30', b'0', 1); +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (7, 1, 2, 1, 'test', '123', '我是 2,我开始 3 了', 1, '{\"name\":\"2\",\"what\":\"3\"}', b'1', '2025-12-08 17:25:22', '1', '2023-01-28 23:45:21', '1', '2025-12-08 17:25:22', b'0', 1); +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (8, 1, 2, 2, 'register', '系统消息', '你好,欢迎 123 加入大家庭!', 2, '{\"name\":\"123\"}', b'1', '2025-12-08 16:46:01', '1', '2023-01-28 23:50:21', '1', '2025-12-08 16:46:01', b'0', 1); INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (9, 247, 1, 4, 'brokerage_withdraw_audit_approve', 'system', '您在2023-09-28 08:35:46提现¥0.09元的申请已通过审核', 2, '{\"reason\":null,\"createTime\":\"2023-09-28 08:35:46\",\"price\":\"0.09\"}', b'0', NULL, '1', '2023-09-28 16:36:22', '1', '2023-09-28 16:36:22', b'0', 1); INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (10, 247, 1, 4, 'brokerage_withdraw_audit_approve', 'system', '您在2023-09-30 20:59:40提现¥1.00元的申请已通过审核', 2, '{\"reason\":null,\"createTime\":\"2023-09-30 20:59:40\",\"price\":\"1.00\"}', b'0', NULL, '1', '2023-10-03 12:11:34', '1', '2023-10-03 12:11:34', b'0', 1); COMMIT; @@ -2448,7 +2462,7 @@ CREATE TABLE `system_oauth2_access_token` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_access_token`(`access_token` ASC) USING BTREE, INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 39737 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌'; +) ENGINE = InnoDB AUTO_INCREMENT = 47630 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌'; -- ---------------------------- -- Records of system_oauth2_access_token @@ -2516,8 +2530,8 @@ CREATE TABLE `system_oauth2_client` ( -- Records of system_oauth2_client -- ---------------------------- BEGIN; -INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 'default', 'admin123', '芋道源码', 'http://test.yudao.iocoder.cn/20250502/sort2_1746189740718.png', '我是描述', 0, 1800, 2592000, '[\"https://www.iocoder.cn\",\"https://doc.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\",\"refresh_token\",\"client_credentials\"]', '[\"user.read\",\"user.write\"]', '[]', '[\"user.read\",\"user.write\"]', '[]', '{}', '1', '2022-05-11 21:47:12', '1', '2025-08-21 10:04:50', b'0'); -INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (40, 'test', 'test2', 'biubiu', 'http://test.yudao.iocoder.cn/xx/20250502/ed07110a37464b5299f8bd7c67ad65c7_1746187077009.jpg', '啦啦啦啦', 0, 1800, 43200, '[\"https://www.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\"]', '[\"user_info\",\"projects\"]', '[\"user_info\"]', '[]', '[]', '{}', '1', '2022-05-12 00:28:20', '1', '2025-05-02 19:58:08', b'0'); +INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 'default', 'admin123', '芋道源码', 'http://test.yudao.iocoder.cn/20250502/sort2_1746189740718.png', '我是描述', 0, 1800, 2592000, '[\"https://www.iocoder.cn\",\"https://doc.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\",\"refresh_token\",\"client_credentials\"]', '[\"user.read\",\"user.write\"]', '[]', '[\"user.read\",\"user.write\"]', '[]', '{}', '1', '2022-05-11 21:47:12', '1', '2025-12-07 20:07:09', b'0'); +INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (40, 'test', 'test2', 'biubiu', 'http://test.yudao.iocoder.cn/20251227/javayuanma_1766829882970.jpg', '啦啦啦啦', 0, 1800, 43200, '[\"https://www.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\"]', '[\"user_info\",\"projects\"]', '[\"user_info\"]', '[]', '[]', '{}', '1', '2022-05-12 00:28:20', '1', '2025-12-27 18:04:44', b'0'); INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (41, 'yudao-sso-demo-by-code', 'test', '基于授权码模式,如何实现 SSO 单点登录?', 'http://test.yudao.iocoder.cn/it/20250502/sign_1746181948685.png', NULL, 0, 1800, 43200, '[\"http://127.0.0.1:18080\"]', '[\"authorization_code\",\"refresh_token\"]', '[\"user.read\",\"user.write\"]', '[]', '[]', '[]', NULL, '1', '2022-09-29 13:28:31', '1', '2025-05-02 18:32:30', b'0'); INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (42, 'yudao-sso-demo-by-password', 'test', '基于密码模式,如何实现 SSO 单点登录?', 'http://test.yudao.iocoder.cn/20251025/images (3)_1761360515810.jpeg', NULL, 0, 1800, 43200, '[\"http://127.0.0.1:18080\"]', '[\"password\",\"refresh_token\"]', '[\"user.read\",\"user.write\"]', '[]', '[]', '[]', NULL, '1', '2022-10-04 17:40:16', '1', '2025-10-25 10:49:40', b'0'); COMMIT; @@ -2570,7 +2584,7 @@ CREATE TABLE `system_oauth2_refresh_token` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2243 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌'; +) ENGINE = InnoDB AUTO_INCREMENT = 2501 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌'; -- ---------------------------- -- Records of system_oauth2_refresh_token @@ -2604,7 +2618,7 @@ CREATE TABLE `system_operate_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 9178 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本'; +) ENGINE = InnoDB AUTO_INCREMENT = 9193 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本'; -- ---------------------------- -- Records of system_operate_log @@ -2636,7 +2650,7 @@ CREATE TABLE `system_post` ( -- Records of system_post -- ---------------------------- BEGIN; -INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 'se', '项目经理', 2, 0, '', 'admin', '2021-01-05 17:03:48', '1', '2023-11-15 09:18:20', b'0', 1); +INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 'se', '项目经理', 2, 0, '', 'admin', '2021-01-05 17:03:48', '1', '2025-12-15 22:38:43', b'0', 1); INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, 'user', '普通员工', 4, 0, '111222', 'admin', '2021-01-05 17:03:48', '1', '2025-03-24 21:32:40', b'0', 1); INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5, 'HR', '人力资源', 5, 0, '`', '1', '2024-03-24 20:45:40', '1', '2025-03-29 19:08:10', b'0', 1); INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (7, 'test', '测试', 10, 0, NULL, '1', '2025-09-02 08:45:57', '1', '2025-09-02 08:45:57', b'0', 1); @@ -2663,7 +2677,7 @@ CREATE TABLE `system_role` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 159 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表'; +) ENGINE = InnoDB AUTO_INCREMENT = 160 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表'; -- ---------------------------- -- Records of system_role @@ -2674,7 +2688,7 @@ INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_sco INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, 'CRM 管理员', 'crm_admin', 2, 1, '', 0, 1, 'CRM 专属角色', '1', '2024-02-24 10:51:13', '1', '2024-02-24 02:51:32', b'0', 1); INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-02-22 00:56:14', '1', '2022-02-22 00:56:14', b'0', 121); INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122); -INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (155, '测试数据权限', 'test-dp', 3, 2, '[112,100,102,103,104,105,107,108]', 0, 2, '', '1', '2025-03-31 14:58:06', '1', '2025-09-06 20:15:13', b'0', 1); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (155, '测试数据权限1', 'test-dp', 4, 2, '[112,100,102,103,104,105,107,108]', 0, 2, '1111', '1', '2025-03-31 14:58:06', '1', '2025-12-04 23:29:40', b'0', 1); COMMIT; -- ---------------------------- @@ -2692,7 +2706,7 @@ CREATE TABLE `system_role_menu` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 6293 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色和菜单关联表'; +) ENGINE = InnoDB AUTO_INCREMENT = 6352 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色和菜单关联表'; -- ---------------------------- -- Records of system_role_menu @@ -3512,6 +3526,65 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6290, 111, 2335, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6291, 111, 2363, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6292, 111, 2364, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6293, 2, 5, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6294, 2, 1118, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6295, 2, 1119, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6296, 2, 1120, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6297, 2, 2713, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6298, 2, 2714, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6299, 2, 2715, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6300, 2, 2716, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6301, 2, 2717, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6302, 2, 2718, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6303, 2, 2720, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6304, 2, 1185, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6305, 2, 2721, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6306, 2, 1186, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6307, 2, 2722, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6308, 2, 1187, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6309, 2, 2723, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6310, 2, 1188, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6311, 2, 2724, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6312, 2, 1189, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6313, 2, 2725, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6314, 2, 1190, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6315, 2, 2726, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6316, 2, 1191, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6317, 2, 2727, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6318, 2, 1192, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6319, 2, 2728, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6320, 2, 1193, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6321, 2, 2729, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6322, 2, 1194, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6323, 2, 2730, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6324, 2, 1195, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6325, 2, 2731, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6326, 2, 2732, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6327, 2, 1197, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6328, 2, 2733, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6329, 2, 1198, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6330, 2, 2734, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6331, 2, 1199, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6332, 2, 2735, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6333, 2, 1200, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6334, 2, 1201, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6335, 2, 1202, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6336, 2, 1207, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6337, 2, 1208, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6338, 2, 1209, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6339, 2, 1210, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6340, 2, 1211, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6341, 2, 1212, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6342, 2, 1213, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6343, 2, 1215, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6344, 2, 1216, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6345, 2, 1217, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6346, 2, 1218, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6347, 2, 1219, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6348, 2, 1220, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6349, 2, 1221, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6350, 2, 1222, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6351, 2, 2913, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1); COMMIT; -- ---------------------------- @@ -3541,7 +3614,7 @@ CREATE TABLE `system_sms_channel` ( BEGIN; INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'Ballcat', 'ALIYUN', 0, '你要改哦,只有我可以用!!!!', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2024-08-04 08:53:26', b'0'); INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '测试渠道', 'DEBUG_DING_TALK', 0, '123', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2021-04-13 00:23:14', '1', '2022-03-27 20:29:49', b'0'); -INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, 'mock腾讯云', 'TENCENT', 0, '', '1 2', '2 3', '', '1', '2024-09-30 08:53:45', '1', '2024-09-30 08:55:01', b'0'); +INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, 'mock腾讯云', 'TENCENT', 0, '123', '1 2', '2 3', '', '1', '2024-09-30 08:53:45', '1', '2025-12-20 11:30:18', b'0'); COMMIT; -- ---------------------------- @@ -3566,7 +3639,7 @@ CREATE TABLE `system_sms_code` ( `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号' -) ENGINE = InnoDB AUTO_INCREMENT = 682 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码'; +) ENGINE = InnoDB AUTO_INCREMENT = 690 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码'; -- ---------------------------- -- Records of system_sms_code @@ -3607,7 +3680,7 @@ CREATE TABLE `system_sms_log` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1528 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志'; +) ENGINE = InnoDB AUTO_INCREMENT = 1549 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志'; -- ---------------------------- -- Records of system_sms_log @@ -3670,9 +3743,9 @@ CREATE TABLE `system_social_client` ( `social_type` tinyint NOT NULL COMMENT '社交平台的类型', `user_type` tinyint NOT NULL COMMENT '用户类型', `client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端编号', - `client_secret` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端密钥', - `public_key` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT 'publicKey公钥', + `client_secret` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端密钥', `agent_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '代理编号', + `public_key` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'publicKey 公钥', `status` tinyint NOT NULL COMMENT '状态', `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', @@ -3681,18 +3754,20 @@ CREATE TABLE `system_social_client` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 46 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交客户端表'; +) ENGINE = InnoDB AUTO_INCREMENT = 48 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交客户端表'; -- ---------------------------- -- Records of system_social_client -- ---------------------------- BEGIN; -INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '钉钉', 20, 2, 'dingvrnreaje3yqvzhxg', 'i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI', NULL, 0, '', '2023-10-18 11:21:18', '1', '2023-12-20 21:28:26', b'1', 1); -INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '钉钉(王土豆)', 20, 2, 'dingtsu9hpepjkbmthhw', 'FP_bnSq_HAHKCSncmJjw5hxhnzs6vaVDSZZn3egj6rdqTQ_hu5tQVJyLMpgCakdP', NULL, 0, '', '2023-10-18 11:21:18', '', '2023-12-20 21:28:26', b'1', 121); -INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, '微信公众号', 31, 1, 'wx5b23ba7a5589ecbb', '2a7b3b20c537e52e74afd395eb85f61f', NULL, 0, '', '2023-10-18 16:07:46', '1', '2023-12-20 21:28:23', b'1', 1); -INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (43, '微信小程序', 34, 1, 'wx63c280fe3248a3e7', '6f270509224a7ae1296bbf1c8cb97aed', NULL, 0, '', '2023-10-19 13:37:41', '1', '2023-12-20 21:28:25', b'1', 1); -INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (44, '1', 10, 1, '2', '3', NULL, 0, '1', '2025-04-06 20:36:28', '1', '2025-04-06 20:43:12', b'1', 1); -INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (45, '1', 10, 1, '2', '3', NULL, 1, '1', '2025-09-06 20:26:15', '1', '2025-09-06 20:27:55', b'1', 1); +INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '钉钉', 20, 2, 'dingvrnreaje3yqvzhxg', 'i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI', NULL, NULL, 0, '', '2023-10-18 11:21:18', '1', '2023-12-20 21:28:26', b'1', 1); +INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '钉钉(王土豆)', 20, 2, 'dingtsu9hpepjkbmthhw', 'FP_bnSq_HAHKCSncmJjw5hxhnzs6vaVDSZZn3egj6rdqTQ_hu5tQVJyLMpgCakdP', NULL, NULL, 0, '', '2023-10-18 11:21:18', '', '2023-12-20 21:28:26', b'1', 121); +INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, '微信公众号', 31, 1, 'wx5b23ba7a5589ecbb', '2a7b3b20c537e52e74afd395eb85f61f', NULL, NULL, 0, '', '2023-10-18 16:07:46', '1', '2023-12-20 21:28:23', b'1', 1); +INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (43, '微信小程序', 34, 1, 'wx63c280fe3248a3e7', '6f270509224a7ae1296bbf1c8cb97aed', NULL, NULL, 0, '', '2023-10-19 13:37:41', '1', '2023-12-20 21:28:25', b'1', 1); +INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (44, '1', 10, 1, '2', '3', NULL, NULL, 0, '1', '2025-04-06 20:36:28', '1', '2025-04-06 20:43:12', b'1', 1); +INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (45, '1', 10, 1, '2', '3', NULL, NULL, 1, '1', '2025-09-06 20:26:15', '1', '2025-09-06 20:27:55', b'1', 1); +INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (46, '1', 10, 1, '2', '3', NULL, NULL, 0, '1', '2025-11-29 16:04:23', '1', '2025-11-29 16:04:26', b'1', 1); +INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (47, '123', 10, 1, '1', '2', '3', NULL, 0, '1', '2025-12-21 10:27:02', '1', '2025-12-21 10:27:20', b'1', 1); COMMIT; -- ---------------------------- @@ -3779,7 +3854,7 @@ CREATE TABLE `system_tenant` ( BEGIN; INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn,127.0.0.1:3000,wxc4598c446f8a9cb3', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2025-08-19 05:18:41', b'0'); INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn,123321', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-08-19 21:19:29', b'0'); -INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn,222,333', 111, '2022-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2025-09-06 20:44:42', b'0'); +INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn,222,333', 111, '2023-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2025-12-21 09:50:00', b'0'); COMMIT; -- ---------------------------- @@ -3822,7 +3897,7 @@ CREATE TABLE `system_user_post` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 128 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户岗位表'; +) ENGINE = InnoDB AUTO_INCREMENT = 130 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户岗位表'; -- ---------------------------- -- Records of system_user_post @@ -3837,6 +3912,8 @@ INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_t INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (123, 115, 1, '1', '2024-04-04 09:37:14', '1', '2024-04-04 09:37:14', b'0', 1); INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (124, 115, 2, '1', '2024-04-04 09:37:14', '1', '2024-04-04 09:37:14', b'0', 1); INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (125, 1, 2, '1', '2024-07-13 22:31:39', '1', '2024-07-13 22:31:39', b'0', 1); +INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (128, 139, 2, '1', '2025-12-05 21:43:27', '1', '2025-12-05 21:43:27', b'0', 1); +INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (129, 139, 4, '1', '2025-12-05 21:43:27', '1', '2025-12-05 21:43:27', b'0', 1); COMMIT; -- ---------------------------- @@ -3854,7 +3931,7 @@ CREATE TABLE `system_user_role` ( `deleted` bit(1) NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户和角色关联表'; +) ENGINE = InnoDB AUTO_INCREMENT = 55 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户和角色关联表'; -- ---------------------------- -- Records of system_user_role @@ -3877,6 +3954,10 @@ INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_t INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (48, 100, 155, '1', '2025-04-04 10:41:14', '1', '2025-04-04 10:41:14', b'0', 1); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (49, 142, 1, '1', '2025-07-23 09:11:42', '1', '2025-07-23 09:11:42', b'0', 1); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (50, 142, 2, '1', '2025-10-07 20:50:37', '1', '2025-10-07 20:50:37', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (51, 139, 1, '1', '2025-12-05 22:36:57', '1', '2025-12-05 22:36:57', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (52, 139, 2, '1', '2025-12-05 22:37:00', '1', '2025-12-05 22:37:00', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (53, 114, 2, '1', '2026-01-04 18:15:40', '1', '2026-01-04 18:15:40', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (54, 114, 3, '1', '2026-01-04 18:16:19', '1', '2026-01-04 18:16:19', b'0', 1); COMMIT; -- ---------------------------- @@ -3905,16 +3986,16 @@ CREATE TABLE `system_users` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 143 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表'; +) ENGINE = InnoDB AUTO_INCREMENT = 145 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表'; -- ---------------------------- -- Records of system_users -- ---------------------------- BEGIN; -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$04$KljJDa/LK7QfDm0lF5OhuePhlPfjRH3tB2Wu351Uidz.oQGJXevPi', '芋道源码', '管理员', 103, '[1,2]', '11aoteman@126.com', '18818260272', 2, 'http://test.yudao.iocoder.cn/20250921/avatar_1758423875594.png', 0, '0:0:0:0:0:0:0:1', '2025-11-22 18:50:21', 'admin', '2021-01-05 17:03:47', NULL, '2025-11-22 18:50:21', b'0', 1); -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$04$h.aaPKgO.odHepnk5PCsWeEwKdojFWdTItxGKfx1r0e1CSeBzsTJ6', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2025-04-08 09:36:40', '', '2021-01-07 09:07:17', NULL, '2025-04-21 14:23:08', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$04$.vd8nPeLwxt6hnSzmAoAyul8BOLX7Cib6QhcxRe30rfvrIPQHH1OG', '芋道源码', '管理员', 103, '[1,2]', '13aoteman@126.com', '18818260272', 1, 'http://test.yudao.iocoder.cn/user/avatar/20251220/blob_1766215463801.jpg', 0, '0:0:0:0:0:0:0:1', '2026-02-14 09:07:33', 'admin', '2021-01-05 17:03:47', NULL, '2026-02-14 09:07:33', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$04$h.aaPKgO.odHepnk5PCsWeEwKdojFWdTItxGKfx1r0e1CSeBzsTJ6', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2025-12-15 21:47:26', '', '2021-01-07 09:07:17', NULL, '2025-12-15 21:47:26', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$04$fUBSmjKCPYAUmnMzOb6qE.eZCGPhHi1JmAKclODbfS/O7fHOl2bH6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, NULL, 0, '0:0:0:0:0:0:0:1', '2024-08-11 17:48:12', '', '2021-01-13 23:50:35', '1', '2025-07-09 23:41:58', b'0', 1); -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$BrwaYn303hjA/6TnXqdGoOLhyHOAA0bVrAFu6.1dJKycqKUnIoRz2', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2025-03-28 20:01:16', '', '2021-01-21 02:13:53', NULL, '2025-04-21 14:23:08', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$BrwaYn303hjA/6TnXqdGoOLhyHOAA0bVrAFu6.1dJKycqKUnIoRz2', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2026-01-04 18:09:54', '', '2021-01-21 02:13:53', NULL, '2026-01-04 18:09:54', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, 'admin107', '$2a$10$dYOOBKMO93v/.ReCqzyFg.o67Tqk.bbc2bhrpyBGkIw9aypCtr2pm', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 22:59:33', '1', '2025-04-21 14:23:08', b'0', 118); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, 'admin108', '$2a$10$y6mfvKoNYL1GXWak8nYwVOH.kCWqjactkzdoIDgiKl93WN3Ejg.Lu', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 23:00:50', '1', '2025-04-21 14:23:08', b'0', 119); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, 'admin109', '$2a$10$JAqvH0tEc0I7dfDVBI7zyuB4E3j.uH6daIjV53.vUS6PknFkDJkuK', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 23:11:50', '1', '2025-04-21 14:23:08', b'0', 120); @@ -3922,13 +4003,15 @@ INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, 'test', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '测试用户', NULL, NULL, '[]', '', '', 0, NULL, 0, '0:0:0:0:0:0:0:1', '2023-12-30 11:42:17', '110', '2022-02-23 13:14:33', NULL, '2025-04-21 14:23:08', b'0', 121); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (112, 'newobject', '$2a$04$dB0z8Q819fJWz0hbaLe6B.VfHCjYgWx6LFfET5lyz3JwcqlyCkQ4C', '新对象', NULL, 100, '[]', '', '15601691235', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2024-03-16 23:11:38', '1', '2022-02-23 19:08:03', NULL, '2025-04-21 14:23:08', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, 'aoteman', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋道1', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '127.0.0.1', '2022-03-19 18:38:51', '1', '2022-03-07 21:37:58', '1', '2025-05-05 15:30:53', b'0', 122); -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (114, 'hrmgr', '$2a$10$TR4eybBioGRhBmDBWkqWLO6NIh3mzYa8KBKDDB5woiGYFVlRAi.fu', 'hr 小姐姐', NULL, NULL, '[5]', '', '15601691236', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2024-03-24 22:21:05', '1', '2022-03-19 21:50:58', NULL, '2025-04-21 14:23:08', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (114, 'hrmgr', '$2a$10$TR4eybBioGRhBmDBWkqWLO6NIh3mzYa8KBKDDB5woiGYFVlRAi.fu', 'hr 小姐姐', NULL, NULL, '[5]', '', '15601691236', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2026-01-04 18:16:01', '1', '2022-03-19 21:50:58', NULL, '2026-01-04 18:16:01', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, 'aotemane', '$2a$04$GcyP0Vyzb2F2Yni5PuIK9ueGxM0tkZGMtDwVRwrNbtMvorzbpNsV2', '阿呆', '11222', 102, '[1,2]', '7648@qq.com', '15601691229', 2, NULL, 0, '', NULL, '1', '2022-04-30 02:55:43', '1', '2025-04-21 14:23:08', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, 'admin123', '$2a$04$sEtimsHu9YCkYY4/oqElHem2Ijc9ld20eYO6lN.g/21NfLUTDLB9W', '测试号02', '1111', 100, '[2]', '', '15601691234', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2024-10-02 10:16:20', '1', '2022-07-09 17:40:26', '1', '2025-05-14 09:56:04', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (118, 'goudan', '$2a$04$3suGZjnA6rM5bErf38u1felbgqbsPHGdRG3l9NkxPCEt2ah9Y6aJi', '狗蛋', NULL, 103, '[1]', '', '15601691239', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2025-11-23 15:28:25', '1', '2022-07-09 17:44:43', NULL, '2025-11-23 15:28:25', b'0', 1); -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (139, 'wwbwwb', '$2a$04$aOHoFbQU6zfBk/1Z9raF/ugTdhjNdx7culC1HhO0zvoczAnahCiMq', '小秃头', NULL, NULL, NULL, '', '', 0, NULL, 0, '0:0:0:0:0:0:0:1', '2024-09-10 21:03:58', NULL, '2024-09-10 21:03:58', NULL, '2025-04-21 14:23:08', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (139, 'wwbwwb', '$2a$04$FJLIyg8lbPytP29pbZaiU.LesJvCsYfEaHqQfB0pGQhK3e9BeZmLy', '小秃头', '123', 108, '[2,4]', '', '', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2024-09-10 21:03:58', NULL, '2024-09-10 21:03:58', '1', '2025-12-15 22:38:15', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (141, 'admin1', '$2a$04$oj6F6d7HrZ70kYVD3TNzEu.m3TPUzajOVuC66zdKna8KRerK1FmVa', '新用户', NULL, NULL, NULL, '', '', 0, '', 0, '0:0:0:0:0:0:0:1', '2025-04-08 13:09:07', '1', '2025-04-08 13:09:07', '1', '2025-05-14 19:11:48', b'0', 1); -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (142, 'test01', '$2a$04$IaR0fGYtalIDURMMdcaD2.4JDWZ15ueQZwap9oPUuxkwSbL66vIRG', 'test01', '', NULL, '[]', '', '19021719925', 1, '', 0, '0:0:0:0:0:0:0:1', '2025-07-29 19:47:17', '1', '2025-07-09 21:07:10', '1', '2025-11-25 19:49:08', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (142, 'test01', '$2a$04$4bCYWZkjxxOC4QE0LY2M9uEEKWeJbLfs489NFtQoyidL5I0FndRaO', 'test01', '', NULL, '[]', '', '19021719925', 1, '', 0, '0:0:0:0:0:0:0:1', '2025-07-29 19:47:17', '1', '2025-07-09 21:07:10', NULL, '2025-12-02 13:23:11', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (143, 'a00001', '$2a$04$GhVHFviOw/SsTmiQtifHJesDYFlHMeGK7OWh7aGCCjGGVCmbHVAwa', 'a00001', NULL, 104, NULL, '', '', 0, '', 0, '0:0:0:0:0:0:0:1', '2025-12-01 16:10:13', NULL, '2025-12-01 16:10:13', '1', '2025-12-05 21:34:05', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (144, 'aoteman001', '$2a$04$omQOmhz8OyUFBKw77nr8KOtMp6xdvoQ1gWStjk9r8.OYT3Bv6oEYe', 'aoteman001', NULL, 116, NULL, '', '', 0, '', 1, '0:0:0:0:0:0:0:1', '2025-12-01 17:05:27', '1', '2025-12-01 17:05:27', '1', '2025-12-15 15:55:54', b'0', 1); COMMIT; -- ---------------------------- From 566f5a12b718beb1c94b0f18234e7351623ad95f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 14 Feb 2026 16:33:27 +0800 Subject: [PATCH 53/53] feat(iot): add j2mod dependency and refactor connection info method --- yudao-dependencies/pom.xml | 8 ++++++++ yudao-module-iot/yudao-module-iot-gateway/pom.xml | 1 - .../protocol/mqtt/manager/IotMqttConnectionManager.java | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 2e8c26e2b2..fa180b4c7c 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -70,6 +70,7 @@ 4.5.22 4.12.0 3.12.0 + 3.2.1 2.40.15 1.16.7 @@ -601,6 +602,13 @@ ${californium.version} + + + com.ghgande + j2mod + ${j2mod.version} + + software.amazon.awssdk diff --git a/yudao-module-iot/yudao-module-iot-gateway/pom.xml b/yudao-module-iot/yudao-module-iot-gateway/pom.xml index 0731198fd7..51e55c3246 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/pom.xml +++ b/yudao-module-iot/yudao-module-iot-gateway/pom.xml @@ -52,7 +52,6 @@ com.ghgande j2mod - 3.2.1 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java index 9bd3ec4934..3d187c847e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java @@ -124,7 +124,7 @@ public class IotMqttConnectionManager { * @param deviceId 设备 ID * @return 连接信息 */ - public IotMqttConnectionManager.ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) { + public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) { // 通过设备 ID 获取连接端点 MqttEndpoint endpoint = getDeviceEndpoint(deviceId); if (endpoint == null) {