Commit bf3b19d3fb9969f300caefe946e6925c6a095f16
Committed by
Gitee
!1498 feat:【iot】modbus-tcp 协议接入:100%
Merge pull request !1498 from 芋道源码/feature/iot-modbus
Showing
133 changed files
with
7297 additions
and
269 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.api.device; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.util.ObjUtil; | |
| 5 | +import cn.hutool.core.util.StrUtil; | |
| 3 | 6 | import cn.iocoder.yudao.framework.common.enums.RpcConstants; |
| 4 | 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 5 | 8 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 9 | +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; | |
| 6 | 10 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 7 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 8 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; | |
| 9 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 10 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; | |
| 11 | +import cn.iocoder.yudao.module.iot.core.biz.dto.*; | |
| 11 | 12 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; |
| 12 | 13 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; |
| 13 | 14 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; |
| 14 | 15 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 16 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; | |
| 17 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; | |
| 15 | 18 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| 19 | +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService; | |
| 20 | +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; | |
| 16 | 21 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| 17 | 22 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; |
| 18 | 23 | import jakarta.annotation.Resource; |
| 19 | 24 | import jakarta.annotation.security.PermitAll; |
| 25 | +import org.springframework.context.annotation.Lazy; | |
| 20 | 26 | import org.springframework.context.annotation.Primary; |
| 21 | 27 | import org.springframework.validation.annotation.Validated; |
| 22 | 28 | import org.springframework.web.bind.annotation.PostMapping; |
| 23 | 29 | import org.springframework.web.bind.annotation.RequestBody; |
| 24 | 30 | import org.springframework.web.bind.annotation.RestController; |
| 25 | 31 | |
| 32 | +import java.util.ArrayList; | |
| 26 | 33 | import java.util.List; |
| 34 | +import java.util.Map; | |
| 35 | +import java.util.Set; | |
| 27 | 36 | |
| 28 | 37 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 38 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; | |
| 29 | 39 | |
| 30 | 40 | /** |
| 31 | 41 | * IoT 设备 API 实现类 |
| ... | ... | @@ -41,6 +51,12 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { |
| 41 | 51 | private IotDeviceService deviceService; |
| 42 | 52 | @Resource |
| 43 | 53 | private IotProductService productService; |
| 54 | + @Resource | |
| 55 | + @Lazy // 延迟加载,解决循环依赖 | |
| 56 | + private IotDeviceModbusConfigService modbusConfigService; | |
| 57 | + @Resource | |
| 58 | + @Lazy // 延迟加载,解决循环依赖 | |
| 59 | + private IotDeviceModbusPointService modbusPointService; | |
| 44 | 60 | |
| 45 | 61 | @Override |
| 46 | 62 | @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth") |
| ... | ... | @@ -64,6 +80,52 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { |
| 64 | 80 | } |
| 65 | 81 | |
| 66 | 82 | @Override |
| 83 | + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/config-list") | |
| 84 | + @PermitAll | |
| 85 | + @TenantIgnore | |
| 86 | + public CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList( | |
| 87 | + @RequestBody IotModbusDeviceConfigListReqDTO listReqDTO) { | |
| 88 | + // 1. 获取 Modbus 连接配置 | |
| 89 | + List<IotDeviceModbusConfigDO> configList = modbusConfigService.getDeviceModbusConfigList(listReqDTO); | |
| 90 | + if (CollUtil.isEmpty(configList)) { | |
| 91 | + return success(new ArrayList<>()); | |
| 92 | + } | |
| 93 | + | |
| 94 | + // 2. 组装返回结果 | |
| 95 | + Set<Long> deviceIds = convertSet(configList, IotDeviceModbusConfigDO::getDeviceId); | |
| 96 | + Map<Long, IotDeviceDO> deviceMap = deviceService.getDeviceMap(deviceIds); | |
| 97 | + Map<Long, List<IotDeviceModbusPointDO>> pointMap = modbusPointService.getEnabledDeviceModbusPointMapByDeviceIds(deviceIds); | |
| 98 | + Map<Long, IotProductDO> productMap = productService.getProductMap(convertSet(deviceMap.values(), IotDeviceDO::getProductId)); | |
| 99 | + List<IotModbusDeviceConfigRespDTO> result = new ArrayList<>(configList.size()); | |
| 100 | + for (IotDeviceModbusConfigDO config : configList) { | |
| 101 | + // 3.1 获取设备信息 | |
| 102 | + IotDeviceDO device = deviceMap.get(config.getDeviceId()); | |
| 103 | + if (device == null) { | |
| 104 | + continue; | |
| 105 | + } | |
| 106 | + // 3.2 按 protocolType 筛选(如果非空) | |
| 107 | + if (StrUtil.isNotEmpty(listReqDTO.getProtocolType())) { | |
| 108 | + IotProductDO product = productMap.get(device.getProductId()); | |
| 109 | + if (product == null || ObjUtil.notEqual(listReqDTO.getProtocolType(), product.getProtocolType())) { | |
| 110 | + continue; | |
| 111 | + } | |
| 112 | + } | |
| 113 | + // 3.3 获取启用的点位列表 | |
| 114 | + List<IotDeviceModbusPointDO> pointList = pointMap.get(config.getDeviceId()); | |
| 115 | + if (CollUtil.isEmpty(pointList)) { | |
| 116 | + continue; | |
| 117 | + } | |
| 118 | + | |
| 119 | + // 3.4 构建 IotModbusDeviceConfigRespDTO 对象 | |
| 120 | + IotModbusDeviceConfigRespDTO configDTO = BeanUtils.toBean(config, IotModbusDeviceConfigRespDTO.class, o -> | |
| 121 | + o.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()) | |
| 122 | + .setPoints(BeanUtils.toBean(pointList, IotModbusPointRespDTO.class))); | |
| 123 | + result.add(configDTO); | |
| 124 | + } | |
| 125 | + return success(result); | |
| 126 | + } | |
| 127 | + | |
| 128 | + @Override | |
| 67 | 129 | @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register") |
| 68 | 130 | @PermitAll |
| 69 | 131 | public CommonResult<IotDeviceRegisterRespDTO> registerDevice(@RequestBody IotDeviceRegisterReqDTO reqDTO) { | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.controller.admin.device; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 4 | +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | |
| 5 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigRespVO; | |
| 6 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; | |
| 7 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; | |
| 8 | +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService; | |
| 9 | +import io.swagger.v3.oas.annotations.Operation; | |
| 10 | +import io.swagger.v3.oas.annotations.Parameter; | |
| 11 | +import io.swagger.v3.oas.annotations.tags.Tag; | |
| 12 | +import jakarta.annotation.Resource; | |
| 13 | +import jakarta.validation.Valid; | |
| 14 | +import org.springframework.security.access.prepost.PreAuthorize; | |
| 15 | +import org.springframework.validation.annotation.Validated; | |
| 16 | +import org.springframework.web.bind.annotation.*; | |
| 17 | + | |
| 18 | +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | |
| 19 | + | |
| 20 | +@Tag(name = "管理后台 - IoT 设备 Modbus 连接配置") | |
| 21 | +@RestController | |
| 22 | +@RequestMapping("/iot/device-modbus-config") | |
| 23 | +@Validated | |
| 24 | +public class IotDeviceModbusConfigController { | |
| 25 | + | |
| 26 | + @Resource | |
| 27 | + private IotDeviceModbusConfigService modbusConfigService; | |
| 28 | + | |
| 29 | + @PostMapping("/save") | |
| 30 | + @Operation(summary = "保存设备 Modbus 连接配置") | |
| 31 | + @PreAuthorize("@ss.hasPermission('iot:device:update')") | |
| 32 | + public CommonResult<Boolean> saveDeviceModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO saveReqVO) { | |
| 33 | + modbusConfigService.saveDeviceModbusConfig(saveReqVO); | |
| 34 | + return success(true); | |
| 35 | + } | |
| 36 | + | |
| 37 | + @GetMapping("/get") | |
| 38 | + @Operation(summary = "获得设备 Modbus 连接配置") | |
| 39 | + @Parameter(name = "id", description = "编号", example = "1024") | |
| 40 | + @Parameter(name = "deviceId", description = "设备编号", example = "2048") | |
| 41 | + @PreAuthorize("@ss.hasPermission('iot:device:query')") | |
| 42 | + public CommonResult<IotDeviceModbusConfigRespVO> getDeviceModbusConfig( | |
| 43 | + @RequestParam(value = "id", required = false) Long id, | |
| 44 | + @RequestParam(value = "deviceId", required = false) Long deviceId) { | |
| 45 | + IotDeviceModbusConfigDO modbusConfig = null; | |
| 46 | + if (id != null) { | |
| 47 | + modbusConfig = modbusConfigService.getDeviceModbusConfig(id); | |
| 48 | + } else if (deviceId != null) { | |
| 49 | + modbusConfig = modbusConfigService.getDeviceModbusConfigByDeviceId(deviceId); | |
| 50 | + } | |
| 51 | + return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class)); | |
| 52 | + } | |
| 53 | + | |
| 54 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.controller.admin.device; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 4 | +import cn.iocoder.yudao.framework.common.pojo.PageResult; | |
| 5 | +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | |
| 6 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; | |
| 7 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointRespVO; | |
| 8 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO; | |
| 9 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; | |
| 10 | +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; | |
| 11 | +import io.swagger.v3.oas.annotations.Operation; | |
| 12 | +import io.swagger.v3.oas.annotations.Parameter; | |
| 13 | +import io.swagger.v3.oas.annotations.tags.Tag; | |
| 14 | +import jakarta.annotation.Resource; | |
| 15 | +import jakarta.validation.Valid; | |
| 16 | +import org.springframework.security.access.prepost.PreAuthorize; | |
| 17 | +import org.springframework.validation.annotation.Validated; | |
| 18 | +import org.springframework.web.bind.annotation.*; | |
| 19 | + | |
| 20 | +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | |
| 21 | + | |
| 22 | +@Tag(name = "管理后台 - IoT 设备 Modbus 点位配置") | |
| 23 | +@RestController | |
| 24 | +@RequestMapping("/iot/device-modbus-point") | |
| 25 | +@Validated | |
| 26 | +public class IotDeviceModbusPointController { | |
| 27 | + | |
| 28 | + @Resource | |
| 29 | + private IotDeviceModbusPointService modbusPointService; | |
| 30 | + | |
| 31 | + @PostMapping("/create") | |
| 32 | + @Operation(summary = "创建设备 Modbus 点位配置") | |
| 33 | + @PreAuthorize("@ss.hasPermission('iot:device:update')") | |
| 34 | + public CommonResult<Long> createDeviceModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO createReqVO) { | |
| 35 | + return success(modbusPointService.createDeviceModbusPoint(createReqVO)); | |
| 36 | + } | |
| 37 | + | |
| 38 | + @PutMapping("/update") | |
| 39 | + @Operation(summary = "更新设备 Modbus 点位配置") | |
| 40 | + @PreAuthorize("@ss.hasPermission('iot:device:update')") | |
| 41 | + public CommonResult<Boolean> updateDeviceModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO updateReqVO) { | |
| 42 | + modbusPointService.updateDeviceModbusPoint(updateReqVO); | |
| 43 | + return success(true); | |
| 44 | + } | |
| 45 | + | |
| 46 | + @DeleteMapping("/delete") | |
| 47 | + @Operation(summary = "删除设备 Modbus 点位配置") | |
| 48 | + @Parameter(name = "id", description = "编号", required = true) | |
| 49 | + @PreAuthorize("@ss.hasPermission('iot:device:update')") | |
| 50 | + public CommonResult<Boolean> deleteDeviceModbusPoint(@RequestParam("id") Long id) { | |
| 51 | + modbusPointService.deleteDeviceModbusPoint(id); | |
| 52 | + return success(true); | |
| 53 | + } | |
| 54 | + | |
| 55 | + @GetMapping("/get") | |
| 56 | + @Operation(summary = "获得设备 Modbus 点位配置") | |
| 57 | + @Parameter(name = "id", description = "编号", required = true, example = "1024") | |
| 58 | + @PreAuthorize("@ss.hasPermission('iot:device:query')") | |
| 59 | + public CommonResult<IotDeviceModbusPointRespVO> getDeviceModbusPoint(@RequestParam("id") Long id) { | |
| 60 | + IotDeviceModbusPointDO modbusPoint = modbusPointService.getDeviceModbusPoint(id); | |
| 61 | + return success(BeanUtils.toBean(modbusPoint, IotDeviceModbusPointRespVO.class)); | |
| 62 | + } | |
| 63 | + | |
| 64 | + @GetMapping("/page") | |
| 65 | + @Operation(summary = "获得设备 Modbus 点位配置分页") | |
| 66 | + @PreAuthorize("@ss.hasPermission('iot:device:query')") | |
| 67 | + public CommonResult<PageResult<IotDeviceModbusPointRespVO>> getDeviceModbusPointPage(@Valid IotDeviceModbusPointPageReqVO pageReqVO) { | |
| 68 | + PageResult<IotDeviceModbusPointDO> pageResult = modbusPointService.getDeviceModbusPointPage(pageReqVO); | |
| 69 | + return success(BeanUtils.toBean(pageResult, IotDeviceModbusPointRespVO.class)); | |
| 70 | + } | |
| 71 | + | |
| 72 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java
| ... | ... | @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageParam; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.validation.InEnum; |
| 5 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; |
| 7 | 7 | import io.swagger.v3.oas.annotations.media.Schema; |
| 8 | 8 | import lombok.Data; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespVO.java
| ... | ... | @@ -38,7 +38,7 @@ public class IotDeviceMessageRespVO { |
| 38 | 38 | @Schema(description = "请求编号", example = "req_123") |
| 39 | 39 | private String requestId; |
| 40 | 40 | |
| 41 | - @Schema(description = "请求方法", requiredMode = Schema.RequiredMode.REQUIRED, example = "thing.property.report") | |
| 41 | + @Schema(description = "请求方法", requiredMode = Schema.RequiredMode.REQUIRED, example = "thing.property.post") | |
| 42 | 42 | private String method; |
| 43 | 43 | |
| 44 | 44 | @Schema(description = "请求参数") | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +import java.time.LocalDateTime; | |
| 7 | + | |
| 8 | +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置 Response VO") | |
| 9 | +@Data | |
| 10 | +public class IotDeviceModbusConfigRespVO { | |
| 11 | + | |
| 12 | + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | |
| 13 | + private Long id; | |
| 14 | + | |
| 15 | + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") | |
| 16 | + private Long deviceId; | |
| 17 | + | |
| 18 | + @Schema(description = "设备名称", example = "温湿度传感器") | |
| 19 | + private String deviceName; | |
| 20 | + | |
| 21 | + @Schema(description = "Modbus 服务器 IP 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100") | |
| 22 | + private String ip; | |
| 23 | + | |
| 24 | + @Schema(description = "Modbus 端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "502") | |
| 25 | + private Integer port; | |
| 26 | + | |
| 27 | + @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | |
| 28 | + private Integer slaveId; | |
| 29 | + | |
| 30 | + @Schema(description = "连接超时时间(毫秒)", example = "3000") | |
| 31 | + private Integer timeout; | |
| 32 | + | |
| 33 | + @Schema(description = "重试间隔(毫秒)", example = "1000") | |
| 34 | + private Integer retryInterval; | |
| 35 | + | |
| 36 | + @Schema(description = "工作模式", example = "1") | |
| 37 | + private Integer mode; | |
| 38 | + | |
| 39 | + @Schema(description = "数据帧格式", example = "1") | |
| 40 | + private Integer frameFormat; | |
| 41 | + | |
| 42 | + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") | |
| 43 | + private Integer status; | |
| 44 | + | |
| 45 | + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) | |
| 46 | + private LocalDateTime createTime; | |
| 47 | + | |
| 48 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.validation.InEnum; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; | |
| 6 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 7 | +import jakarta.validation.constraints.NotNull; | |
| 8 | +import lombok.Data; | |
| 9 | + | |
| 10 | +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置新增/修改 Request VO") | |
| 11 | +@Data | |
| 12 | +public class IotDeviceModbusConfigSaveReqVO { | |
| 13 | + | |
| 14 | + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") | |
| 15 | + @NotNull(message = "设备编号不能为空") | |
| 16 | + private Long deviceId; | |
| 17 | + | |
| 18 | + @Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100") | |
| 19 | + private String ip; | |
| 20 | + | |
| 21 | + @Schema(description = "Modbus 端口", example = "502") | |
| 22 | + private Integer port; | |
| 23 | + | |
| 24 | + @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | |
| 25 | + @NotNull(message = "从站地址不能为空") | |
| 26 | + private Integer slaveId; | |
| 27 | + | |
| 28 | + @Schema(description = "连接超时时间(毫秒)", example = "3000") | |
| 29 | + private Integer timeout; | |
| 30 | + | |
| 31 | + @Schema(description = "重试间隔(毫秒)", example = "1000") | |
| 32 | + private Integer retryInterval; | |
| 33 | + | |
| 34 | + @Schema(description = "工作模式", example = "1") | |
| 35 | + @InEnum(IotModbusModeEnum.class) | |
| 36 | + private Integer mode; | |
| 37 | + | |
| 38 | + @Schema(description = "数据帧格式", example = "1") | |
| 39 | + @InEnum(IotModbusFrameFormatEnum.class) | |
| 40 | + private Integer frameFormat; | |
| 41 | + | |
| 42 | + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") | |
| 43 | + @NotNull(message = "状态不能为空") | |
| 44 | + private Integer status; | |
| 45 | + | |
| 46 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.pojo.PageParam; | |
| 4 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 5 | +import lombok.Data; | |
| 6 | +import lombok.EqualsAndHashCode; | |
| 7 | +import lombok.ToString; | |
| 8 | + | |
| 9 | +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置分页 Request VO") | |
| 10 | +@Data | |
| 11 | +@EqualsAndHashCode(callSuper = true) | |
| 12 | +@ToString(callSuper = true) | |
| 13 | +public class IotDeviceModbusPointPageReqVO extends PageParam { | |
| 14 | + | |
| 15 | + @Schema(description = "设备编号", example = "1024") | |
| 16 | + private Long deviceId; | |
| 17 | + | |
| 18 | + @Schema(description = "属性标识符", example = "temperature") | |
| 19 | + private String identifier; | |
| 20 | + | |
| 21 | + @Schema(description = "属性名称", example = "温度") | |
| 22 | + private String name; | |
| 23 | + | |
| 24 | + @Schema(description = "Modbus 功能码", example = "3") | |
| 25 | + private Integer functionCode; | |
| 26 | + | |
| 27 | + @Schema(description = "状态", example = "0") | |
| 28 | + private Integer status; | |
| 29 | + | |
| 30 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +import java.math.BigDecimal; | |
| 7 | +import java.time.LocalDateTime; | |
| 8 | + | |
| 9 | +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置 Response VO") | |
| 10 | +@Data | |
| 11 | +public class IotDeviceModbusPointRespVO { | |
| 12 | + | |
| 13 | + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | |
| 14 | + private Long id; | |
| 15 | + | |
| 16 | + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") | |
| 17 | + private Long deviceId; | |
| 18 | + | |
| 19 | + @Schema(description = "物模型属性编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") | |
| 20 | + private Long thingModelId; | |
| 21 | + | |
| 22 | + @Schema(description = "属性标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature") | |
| 23 | + private String identifier; | |
| 24 | + | |
| 25 | + @Schema(description = "属性名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "温度") | |
| 26 | + private String name; | |
| 27 | + | |
| 28 | + @Schema(description = "Modbus 功能码", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") | |
| 29 | + private Integer functionCode; | |
| 30 | + | |
| 31 | + @Schema(description = "寄存器起始地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") | |
| 32 | + private Integer registerAddress; | |
| 33 | + | |
| 34 | + @Schema(description = "寄存器数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | |
| 35 | + private Integer registerCount; | |
| 36 | + | |
| 37 | + @Schema(description = "字节序", requiredMode = Schema.RequiredMode.REQUIRED, example = "AB") | |
| 38 | + private String byteOrder; | |
| 39 | + | |
| 40 | + @Schema(description = "原始数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "INT16") | |
| 41 | + private String rawDataType; | |
| 42 | + | |
| 43 | + @Schema(description = "缩放因子", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0") | |
| 44 | + private BigDecimal scale; | |
| 45 | + | |
| 46 | + @Schema(description = "轮询间隔(毫秒)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5000") | |
| 47 | + private Integer pollInterval; | |
| 48 | + | |
| 49 | + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") | |
| 50 | + private Integer status; | |
| 51 | + | |
| 52 | + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) | |
| 53 | + private LocalDateTime createTime; | |
| 54 | + | |
| 55 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import jakarta.validation.constraints.NotEmpty; | |
| 5 | +import jakarta.validation.constraints.NotNull; | |
| 6 | +import lombok.Data; | |
| 7 | + | |
| 8 | +import java.math.BigDecimal; | |
| 9 | + | |
| 10 | +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置新增/修改 Request VO") | |
| 11 | +@Data | |
| 12 | +public class IotDeviceModbusPointSaveReqVO { | |
| 13 | + | |
| 14 | + @Schema(description = "主键", example = "1") | |
| 15 | + private Long id; | |
| 16 | + | |
| 17 | + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") | |
| 18 | + @NotNull(message = "设备编号不能为空") | |
| 19 | + private Long deviceId; | |
| 20 | + | |
| 21 | + @Schema(description = "物模型属性编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") | |
| 22 | + @NotNull(message = "物模型属性编号不能为空") | |
| 23 | + private Long thingModelId; | |
| 24 | + | |
| 25 | + @Schema(description = "Modbus 功能码", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") | |
| 26 | + @NotNull(message = "Modbus 功能码不能为空") | |
| 27 | + private Integer functionCode; | |
| 28 | + | |
| 29 | + @Schema(description = "寄存器起始地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") | |
| 30 | + @NotNull(message = "寄存器起始地址不能为空") | |
| 31 | + private Integer registerAddress; | |
| 32 | + | |
| 33 | + @Schema(description = "寄存器数量", example = "1") | |
| 34 | + private Integer registerCount; | |
| 35 | + | |
| 36 | + @Schema(description = "字节序", requiredMode = Schema.RequiredMode.REQUIRED, example = "AB") | |
| 37 | + @NotEmpty(message = "字节序不能为空") | |
| 38 | + private String byteOrder; | |
| 39 | + | |
| 40 | + @Schema(description = "原始数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "INT16") | |
| 41 | + @NotEmpty(message = "原始数据类型不能为空") | |
| 42 | + private String rawDataType; | |
| 43 | + | |
| 44 | + @Schema(description = "缩放因子", example = "1.0") | |
| 45 | + private BigDecimal scale; | |
| 46 | + | |
| 47 | + @Schema(description = "轮询间隔(毫秒)", example = "5000") | |
| 48 | + private Integer pollInterval; | |
| 49 | + | |
| 50 | + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") | |
| 51 | + @NotNull(message = "状态不能为空") | |
| 52 | + private Integer status; | |
| 53 | + | |
| 54 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.http
0 → 100644
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java
| ... | ... | @@ -141,6 +141,14 @@ public class IotProductController { |
| 141 | 141 | result.getData().getList()); |
| 142 | 142 | } |
| 143 | 143 | |
| 144 | + @PostMapping("/sync-property-table") | |
| 145 | + @Operation(summary = "同步产品属性表结构到 TDengine") | |
| 146 | + @PreAuthorize("@ss.hasPermission('iot:product:update')") | |
| 147 | + public CommonResult<Boolean> syncProductPropertyTable() { | |
| 148 | + productService.syncProductPropertyTable(); | |
| 149 | + return success(true); | |
| 150 | + } | |
| 151 | + | |
| 144 | 152 | @GetMapping("/simple-list") |
| 145 | 153 | @Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项") |
| 146 | 154 | @Parameter(name = "deviceType", description = "设备类型", example = "1") | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java
| ... | ... | @@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsSummaryRespVO; |
| 8 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | |
| 9 | 9 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; |
| 11 | 11 | import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java
| ... | ... | @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.mybatis.core.type.LongSetTypeHandler; |
| 4 | 4 | import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| 7 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | |
| 8 | 8 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 9 | 9 | import com.baomidou.mybatisplus.annotation.TableField; |
| 10 | 10 | import com.baomidou.mybatisplus.annotation.TableId; |
| ... | ... | @@ -109,10 +109,6 @@ public class IotDeviceDO extends TenantBaseDO { |
| 109 | 109 | private LocalDateTime activeTime; |
| 110 | 110 | |
| 111 | 111 | /** |
| 112 | - * 设备的 IP 地址 | |
| 113 | - */ | |
| 114 | - private String ip; | |
| 115 | - /** | |
| 116 | 112 | * 固件编号 |
| 117 | 113 | * |
| 118 | 114 | * 关联 {@link IotOtaFirmwareDO#getId()} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.dal.dataobject.device; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; | |
| 6 | +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; | |
| 7 | +import com.baomidou.mybatisplus.annotation.KeySequence; | |
| 8 | +import com.baomidou.mybatisplus.annotation.TableId; | |
| 9 | +import com.baomidou.mybatisplus.annotation.TableName; | |
| 10 | +import lombok.*; | |
| 11 | + | |
| 12 | +/** | |
| 13 | + * IoT 设备 Modbus 连接配置 DO | |
| 14 | + * | |
| 15 | + * @author 芋道源码 | |
| 16 | + */ | |
| 17 | +@TableName("iot_device_modbus_config") | |
| 18 | +@KeySequence("iot_device_modbus_config_seq") | |
| 19 | +@Data | |
| 20 | +@Builder | |
| 21 | +@NoArgsConstructor | |
| 22 | +@AllArgsConstructor | |
| 23 | +public class IotDeviceModbusConfigDO extends TenantBaseDO { | |
| 24 | + | |
| 25 | + /** | |
| 26 | + * 主键 | |
| 27 | + */ | |
| 28 | + @TableId | |
| 29 | + private Long id; | |
| 30 | + /** | |
| 31 | + * 产品编号 | |
| 32 | + * | |
| 33 | + * 关联 {@link IotProductDO#getId()} | |
| 34 | + */ | |
| 35 | + private Long productId; | |
| 36 | + /** | |
| 37 | + * 设备编号 | |
| 38 | + * | |
| 39 | + * 关联 {@link IotDeviceDO#getId()} | |
| 40 | + */ | |
| 41 | + private Long deviceId; | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * Modbus 服务器 IP 地址 | |
| 45 | + */ | |
| 46 | + private String ip; | |
| 47 | + /** | |
| 48 | + * Modbus 服务器端口 | |
| 49 | + */ | |
| 50 | + private Integer port; | |
| 51 | + /** | |
| 52 | + * 从站地址 | |
| 53 | + */ | |
| 54 | + private Integer slaveId; | |
| 55 | + /** | |
| 56 | + * 连接超时时间,单位:毫秒 | |
| 57 | + */ | |
| 58 | + private Integer timeout; | |
| 59 | + /** | |
| 60 | + * 重试间隔,单位:毫秒 | |
| 61 | + */ | |
| 62 | + private Integer retryInterval; | |
| 63 | + /** | |
| 64 | + * 模式 | |
| 65 | + * | |
| 66 | + * @see IotModbusModeEnum | |
| 67 | + */ | |
| 68 | + private Integer mode; | |
| 69 | + /** | |
| 70 | + * 数据帧格式 | |
| 71 | + * | |
| 72 | + * @see IotModbusFrameFormatEnum | |
| 73 | + */ | |
| 74 | + private Integer frameFormat; | |
| 75 | + /** | |
| 76 | + * 状态 | |
| 77 | + * | |
| 78 | + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} | |
| 79 | + */ | |
| 80 | + private Integer status; | |
| 81 | + | |
| 82 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.dal.dataobject.device; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; | |
| 4 | +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum; | |
| 7 | +import com.baomidou.mybatisplus.annotation.KeySequence; | |
| 8 | +import com.baomidou.mybatisplus.annotation.TableId; | |
| 9 | +import com.baomidou.mybatisplus.annotation.TableName; | |
| 10 | +import lombok.*; | |
| 11 | + | |
| 12 | +import java.math.BigDecimal; | |
| 13 | + | |
| 14 | +/** | |
| 15 | + * IoT 设备 Modbus 点位配置 DO | |
| 16 | + * | |
| 17 | + * @author 芋道源码 | |
| 18 | + */ | |
| 19 | +@TableName("iot_device_modbus_point") | |
| 20 | +@KeySequence("iot_device_modbus_point_seq") | |
| 21 | +@Data | |
| 22 | +@Builder | |
| 23 | +@NoArgsConstructor | |
| 24 | +@AllArgsConstructor | |
| 25 | +public class IotDeviceModbusPointDO extends TenantBaseDO { | |
| 26 | + | |
| 27 | + /** | |
| 28 | + * 主键 | |
| 29 | + */ | |
| 30 | + @TableId | |
| 31 | + private Long id; | |
| 32 | + /** | |
| 33 | + * 设备编号 | |
| 34 | + * | |
| 35 | + * 关联 {@link IotDeviceDO#getId()} | |
| 36 | + */ | |
| 37 | + private Long deviceId; | |
| 38 | + /** | |
| 39 | + * 物模型属性编号 | |
| 40 | + * | |
| 41 | + * 关联 {@link IotThingModelDO#getId()} | |
| 42 | + */ | |
| 43 | + private Long thingModelId; | |
| 44 | + /** | |
| 45 | + * 属性标识符 | |
| 46 | + * | |
| 47 | + * 冗余 {@link IotThingModelDO#getIdentifier()} | |
| 48 | + */ | |
| 49 | + private String identifier; | |
| 50 | + /** | |
| 51 | + * 属性名称 | |
| 52 | + * | |
| 53 | + * 冗余 {@link IotThingModelDO#getName()} | |
| 54 | + */ | |
| 55 | + private String name; | |
| 56 | + | |
| 57 | + // ========== Modbus 协议配置 ========== | |
| 58 | + | |
| 59 | + /** | |
| 60 | + * Modbus 功能码 | |
| 61 | + * | |
| 62 | + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器) | |
| 63 | + */ | |
| 64 | + private Integer functionCode; | |
| 65 | + /** | |
| 66 | + * 寄存器起始地址 | |
| 67 | + */ | |
| 68 | + private Integer registerAddress; | |
| 69 | + /** | |
| 70 | + * 寄存器数量 | |
| 71 | + */ | |
| 72 | + private Integer registerCount; | |
| 73 | + /** | |
| 74 | + * 字节序 | |
| 75 | + * | |
| 76 | + * 枚举 {@link IotModbusByteOrderEnum} | |
| 77 | + */ | |
| 78 | + private String byteOrder; | |
| 79 | + /** | |
| 80 | + * 原始数据类型 | |
| 81 | + * | |
| 82 | + * 枚举 {@link IotModbusRawDataTypeEnum} | |
| 83 | + */ | |
| 84 | + private String rawDataType; | |
| 85 | + /** | |
| 86 | + * 缩放因子 | |
| 87 | + */ | |
| 88 | + private BigDecimal scale; | |
| 89 | + /** | |
| 90 | + * 轮询间隔(毫秒) | |
| 91 | + */ | |
| 92 | + private Integer pollInterval; | |
| 93 | + /** | |
| 94 | + * 状态 | |
| 95 | + * | |
| 96 | + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} | |
| 97 | + */ | |
| 98 | + private Integer status; | |
| 99 | + | |
| 100 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.dal.mysql.device; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | |
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; | |
| 6 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; | |
| 7 | +import org.apache.ibatis.annotations.Mapper; | |
| 8 | + | |
| 9 | +import java.util.List; | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * IoT 设备 Modbus 连接配置 Mapper | |
| 13 | + * | |
| 14 | + * @author 芋道源码 | |
| 15 | + */ | |
| 16 | +@Mapper | |
| 17 | +public interface IotDeviceModbusConfigMapper extends BaseMapperX<IotDeviceModbusConfigDO> { | |
| 18 | + | |
| 19 | + default IotDeviceModbusConfigDO selectByDeviceId(Long deviceId) { | |
| 20 | + return selectOne(IotDeviceModbusConfigDO::getDeviceId, deviceId); | |
| 21 | + } | |
| 22 | + | |
| 23 | + default List<IotDeviceModbusConfigDO> selectList(IotModbusDeviceConfigListReqDTO reqDTO) { | |
| 24 | + return selectList(new LambdaQueryWrapperX<IotDeviceModbusConfigDO>() | |
| 25 | + .eqIfPresent(IotDeviceModbusConfigDO::getStatus, reqDTO.getStatus()) | |
| 26 | + .eqIfPresent(IotDeviceModbusConfigDO::getMode, reqDTO.getMode()) | |
| 27 | + .inIfPresent(IotDeviceModbusConfigDO::getDeviceId, reqDTO.getDeviceIds())); | |
| 28 | + } | |
| 29 | + | |
| 30 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.dal.mysql.device; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.pojo.PageResult; | |
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | |
| 5 | +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | |
| 6 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; | |
| 7 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; | |
| 8 | +import org.apache.ibatis.annotations.Mapper; | |
| 9 | + | |
| 10 | +import java.util.Collection; | |
| 11 | +import java.util.List; | |
| 12 | + | |
| 13 | +/** | |
| 14 | + * IoT 设备 Modbus 点位配置 Mapper | |
| 15 | + * | |
| 16 | + * @author 芋道源码 | |
| 17 | + */ | |
| 18 | +@Mapper | |
| 19 | +public interface IotDeviceModbusPointMapper extends BaseMapperX<IotDeviceModbusPointDO> { | |
| 20 | + | |
| 21 | + default PageResult<IotDeviceModbusPointDO> selectPage(IotDeviceModbusPointPageReqVO reqVO) { | |
| 22 | + return selectPage(reqVO, new LambdaQueryWrapperX<IotDeviceModbusPointDO>() | |
| 23 | + .eqIfPresent(IotDeviceModbusPointDO::getDeviceId, reqVO.getDeviceId()) | |
| 24 | + .likeIfPresent(IotDeviceModbusPointDO::getIdentifier, reqVO.getIdentifier()) | |
| 25 | + .likeIfPresent(IotDeviceModbusPointDO::getName, reqVO.getName()) | |
| 26 | + .eqIfPresent(IotDeviceModbusPointDO::getFunctionCode, reqVO.getFunctionCode()) | |
| 27 | + .eqIfPresent(IotDeviceModbusPointDO::getStatus, reqVO.getStatus()) | |
| 28 | + .orderByDesc(IotDeviceModbusPointDO::getId)); | |
| 29 | + } | |
| 30 | + | |
| 31 | + default List<IotDeviceModbusPointDO> selectListByDeviceIdsAndStatus(Collection<Long> deviceIds, Integer status) { | |
| 32 | + return selectList(new LambdaQueryWrapperX<IotDeviceModbusPointDO>() | |
| 33 | + .in(IotDeviceModbusPointDO::getDeviceId, deviceIds) | |
| 34 | + .eq(IotDeviceModbusPointDO::getStatus, status)); | |
| 35 | + } | |
| 36 | + | |
| 37 | + default IotDeviceModbusPointDO selectByDeviceIdAndIdentifier(Long deviceId, String identifier) { | |
| 38 | + return selectOne(IotDeviceModbusPointDO::getDeviceId, deviceId, | |
| 39 | + IotDeviceModbusPointDO::getIdentifier, identifier); | |
| 40 | + } | |
| 41 | + | |
| 42 | + default void updateByThingModelId(Long thingModelId, IotDeviceModbusPointDO updateObj) { | |
| 43 | + update(updateObj, new LambdaQueryWrapperX<IotDeviceModbusPointDO>() | |
| 44 | + .eq(IotDeviceModbusPointDO::getThingModelId, thingModelId)); | |
| 45 | + } | |
| 46 | + | |
| 47 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java
| ... | ... | @@ -38,6 +38,10 @@ public interface IotProductMapper extends BaseMapperX<IotProductDO> { |
| 38 | 38 | .apply("LOWER(product_key) = {0}", productKey.toLowerCase())); |
| 39 | 39 | } |
| 40 | 40 | |
| 41 | + default List<IotProductDO> selectListByStatus(Integer status) { | |
| 42 | + return selectList(IotProductDO::getStatus, status); | |
| 43 | + } | |
| 44 | + | |
| 41 | 45 | default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) { |
| 42 | 46 | return selectCount(new LambdaQueryWrapperX<IotProductDO>() |
| 43 | 47 | .geIfPresent(IotProductDO::getCreateTime, createTime)); | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
| ... | ... | @@ -54,6 +54,14 @@ public interface ErrorCodeConstants { |
| 54 | 54 | ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); |
| 55 | 55 | ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); |
| 56 | 56 | |
| 57 | + // ========== 设备 Modbus 配置 1-050-006-000 ========== | |
| 58 | + ErrorCode DEVICE_MODBUS_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "设备 Modbus 连接配置不存在"); | |
| 59 | + ErrorCode DEVICE_MODBUS_CONFIG_EXISTS = new ErrorCode(1_050_006_001, "设备 Modbus 连接配置已存在"); | |
| 60 | + | |
| 61 | + // ========== 设备 Modbus 点位 1-050-007-000 ========== | |
| 62 | + ErrorCode DEVICE_MODBUS_POINT_NOT_EXISTS = new ErrorCode(1_050_007_000, "设备 Modbus 点位配置不存在"); | |
| 63 | + ErrorCode DEVICE_MODBUS_POINT_EXISTS = new ErrorCode(1_050_007_001, "设备 Modbus 点位配置已存在"); | |
| 64 | + | |
| 57 | 65 | // ========== OTA 固件相关 1-050-008-000 ========== |
| 58 | 66 | |
| 59 | 67 | ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java
| ... | ... | @@ -19,9 +19,9 @@ public enum IotDataSinkTypeEnum implements ArrayValuable<Integer> { |
| 19 | 19 | TCP(2, "TCP"), |
| 20 | 20 | WEBSOCKET(3, "WebSocket"), |
| 21 | 21 | |
| 22 | - MQTT(10, "MQTT"), // TODO 待实现; | |
| 22 | + MQTT(10, "MQTT"), // TODO @puhui999:待实现; | |
| 23 | 23 | |
| 24 | - DATABASE(20, "Database"), // TODO @puhui999:待实现;可以简单点,对应的表名是什么,字段先固定了。 | |
| 24 | + DATABASE(20, "Database"), // TODO @puhui999:待实现; | |
| 25 | 25 | REDIS(21, "Redis"), |
| 26 | 26 | |
| 27 | 27 | ROCKETMQ(30, "RocketMQ"), | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java
| ... | ... | @@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 5 | 5 | import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; |
| 6 | 6 | import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; |
| 7 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | |
| 8 | 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.framework.iot.config.YudaoIotProperties; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java
| ... | ... | @@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.util.StrUtil; |
| 5 | 5 | import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; |
| 6 | 6 | import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; |
| 7 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | |
| 8 | 8 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java
| ... | ... | @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.mq.consumer.device; |
| 3 | 3 | import cn.hutool.core.util.ObjectUtil; |
| 4 | 4 | import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 6 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| ... | ... | @@ -67,7 +67,6 @@ public class IotDeviceMessageSubscriber implements IotMessageSubscriber<IotDevic |
| 67 | 67 | IotDeviceDO device = deviceService.validateDeviceExistsFromCache(message.getDeviceId()); |
| 68 | 68 | devicePropertyService.updateDeviceReportTimeAsync(device.getId(), LocalDateTime.now()); |
| 69 | 69 | // 1.2 更新设备的连接 server |
| 70 | - // TODO 芋艿:HTTP 网关的上行消息,不应该更新 serverId,会覆盖掉 MQTT 等长连接的 serverId,导致下行消息无法发送。 | |
| 71 | 70 | devicePropertyService.updateDeviceServerIdAsync(device.getId(), message.getServerId()); |
| 72 | 71 | |
| 73 | 72 | // 2. 未上线的设备,强制上线 | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotDataRuleMessageHandler.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotDataRuleMessageSubscriber.java
| ... | ... | @@ -17,7 +17,7 @@ import org.springframework.stereotype.Component; |
| 17 | 17 | */ |
| 18 | 18 | @Component |
| 19 | 19 | @Slf4j |
| 20 | -public class IotDataRuleMessageHandler implements IotMessageSubscriber<IotDeviceMessage> { | |
| 20 | +public class IotDataRuleMessageSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | |
| 21 | 21 | |
| 22 | 22 | @Resource |
| 23 | 23 | private IotDataRuleService dataRuleService; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageHandler.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageSubscriber.java
| ... | ... | @@ -9,7 +9,6 @@ import jakarta.annotation.Resource; |
| 9 | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | 10 | import org.springframework.stereotype.Component; |
| 11 | 11 | |
| 12 | -// TODO @puhui999:后面重构哈 | |
| 13 | 12 | /** |
| 14 | 13 | * 针对 {@link IotDeviceMessage} 的消费者,处理规则场景 |
| 15 | 14 | * |
| ... | ... | @@ -17,7 +16,7 @@ import org.springframework.stereotype.Component; |
| 17 | 16 | */ |
| 18 | 17 | @Component |
| 19 | 18 | @Slf4j |
| 20 | -public class IotSceneRuleMessageHandler implements IotMessageSubscriber<IotDeviceMessage> { | |
| 19 | +public class IotSceneRuleMessageSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | |
| 21 | 20 | |
| 22 | 21 | @Resource |
| 23 | 22 | private IotSceneRuleService sceneRuleService; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.service.device; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; | |
| 5 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; | |
| 6 | +import jakarta.validation.Valid; | |
| 7 | + | |
| 8 | +import java.util.List; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * IoT 设备 Modbus 连接配置 Service 接口 | |
| 12 | + * | |
| 13 | + * @author 芋道源码 | |
| 14 | + */ | |
| 15 | +public interface IotDeviceModbusConfigService { | |
| 16 | + | |
| 17 | + /** | |
| 18 | + * 保存设备 Modbus 连接配置(新增或更新) | |
| 19 | + * | |
| 20 | + * @param saveReqVO 保存信息 | |
| 21 | + */ | |
| 22 | + void saveDeviceModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO saveReqVO); | |
| 23 | + | |
| 24 | + /** | |
| 25 | + * 获得设备 Modbus 连接配置 | |
| 26 | + * | |
| 27 | + * @param id 编号 | |
| 28 | + * @return 设备 Modbus 连接配置 | |
| 29 | + */ | |
| 30 | + IotDeviceModbusConfigDO getDeviceModbusConfig(Long id); | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 根据设备编号获得 Modbus 连接配置 | |
| 34 | + * | |
| 35 | + * @param deviceId 设备编号 | |
| 36 | + * @return 设备 Modbus 连接配置 | |
| 37 | + */ | |
| 38 | + IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId); | |
| 39 | + | |
| 40 | + /** | |
| 41 | + * 获得 Modbus 连接配置列表 | |
| 42 | + * | |
| 43 | + * @param listReqDTO 查询参数 | |
| 44 | + * @return Modbus 连接配置列表 | |
| 45 | + */ | |
| 46 | + List<IotDeviceModbusConfigDO> getDeviceModbusConfigList(IotModbusDeviceConfigListReqDTO listReqDTO); | |
| 47 | + | |
| 48 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.service.device; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 5 | +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | |
| 6 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 9 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; | |
| 10 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; | |
| 11 | +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; | |
| 12 | +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusConfigMapper; | |
| 13 | +import cn.iocoder.yudao.module.iot.service.product.IotProductService; | |
| 14 | +import jakarta.annotation.Resource; | |
| 15 | +import org.springframework.stereotype.Service; | |
| 16 | +import org.springframework.validation.annotation.Validated; | |
| 17 | + | |
| 18 | +import java.util.List; | |
| 19 | + | |
| 20 | +/** | |
| 21 | + * IoT 设备 Modbus 连接配置 Service 实现类 | |
| 22 | + * | |
| 23 | + * @author 芋道源码 | |
| 24 | + */ | |
| 25 | +@Service | |
| 26 | +@Validated | |
| 27 | +public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigService { | |
| 28 | + | |
| 29 | + @Resource | |
| 30 | + private IotDeviceModbusConfigMapper modbusConfigMapper; | |
| 31 | + | |
| 32 | + @Resource | |
| 33 | + private IotDeviceService deviceService; | |
| 34 | + @Resource | |
| 35 | + private IotProductService productService; | |
| 36 | + | |
| 37 | + @Override | |
| 38 | + public void saveDeviceModbusConfig(IotDeviceModbusConfigSaveReqVO saveReqVO) { | |
| 39 | + // 1.1 校验设备存在 | |
| 40 | + IotDeviceDO device = deviceService.validateDeviceExists(saveReqVO.getDeviceId()); | |
| 41 | + // 1.2 根据产品 protocolType 条件校验 | |
| 42 | + IotProductDO product = productService.getProduct(device.getProductId()); | |
| 43 | + Assert.notNull(product, "产品不存在"); | |
| 44 | + validateModbusConfigByProtocolType(saveReqVO, product.getProtocolType()); | |
| 45 | + | |
| 46 | + // 2. 根据数据库中是否已有配置,决定是新增还是更新 | |
| 47 | + IotDeviceModbusConfigDO existConfig = modbusConfigMapper.selectByDeviceId(saveReqVO.getDeviceId()); | |
| 48 | + if (existConfig == null) { | |
| 49 | + IotDeviceModbusConfigDO modbusConfig = BeanUtils.toBean(saveReqVO, IotDeviceModbusConfigDO.class); | |
| 50 | + modbusConfigMapper.insert(modbusConfig); | |
| 51 | + } else { | |
| 52 | + IotDeviceModbusConfigDO updateObj = BeanUtils.toBean(saveReqVO, IotDeviceModbusConfigDO.class, | |
| 53 | + o -> o.setId(existConfig.getId())); | |
| 54 | + modbusConfigMapper.updateById(updateObj); | |
| 55 | + } | |
| 56 | + } | |
| 57 | + | |
| 58 | + @Override | |
| 59 | + public IotDeviceModbusConfigDO getDeviceModbusConfig(Long id) { | |
| 60 | + return modbusConfigMapper.selectById(id); | |
| 61 | + } | |
| 62 | + | |
| 63 | + @Override | |
| 64 | + public IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId) { | |
| 65 | + return modbusConfigMapper.selectByDeviceId(deviceId); | |
| 66 | + } | |
| 67 | + | |
| 68 | + @Override | |
| 69 | + public List<IotDeviceModbusConfigDO> getDeviceModbusConfigList(IotModbusDeviceConfigListReqDTO listReqDTO) { | |
| 70 | + return modbusConfigMapper.selectList(listReqDTO); | |
| 71 | + } | |
| 72 | + | |
| 73 | + private void validateModbusConfigByProtocolType(IotDeviceModbusConfigSaveReqVO saveReqVO, String protocolType) { | |
| 74 | + IotProtocolTypeEnum protocolTypeEnum = IotProtocolTypeEnum.of(protocolType); | |
| 75 | + if (protocolTypeEnum == null) { | |
| 76 | + return; | |
| 77 | + } | |
| 78 | + if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_CLIENT) { | |
| 79 | + Assert.isTrue(StrUtil.isNotEmpty(saveReqVO.getIp()), "Client 模式下,IP 地址不能为空"); | |
| 80 | + Assert.notNull(saveReqVO.getPort(), "Client 模式下,端口不能为空"); | |
| 81 | + Assert.notNull(saveReqVO.getTimeout(), "Client 模式下,连接超时时间不能为空"); | |
| 82 | + Assert.notNull(saveReqVO.getRetryInterval(), "Client 模式下,重试间隔不能为空"); | |
| 83 | + } else if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_SERVER) { | |
| 84 | + Assert.notNull(saveReqVO.getMode(), "Server 模式下,工作模式不能为空"); | |
| 85 | + Assert.notNull(saveReqVO.getFrameFormat(), "Server 模式下,数据帧格式不能为空"); | |
| 86 | + } | |
| 87 | + } | |
| 88 | + | |
| 89 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.service.device; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.pojo.PageResult; | |
| 4 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; | |
| 5 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO; | |
| 6 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; | |
| 7 | +import jakarta.validation.Valid; | |
| 8 | + | |
| 9 | +import java.util.Collection; | |
| 10 | +import java.util.List; | |
| 11 | +import java.util.Map; | |
| 12 | + | |
| 13 | +/** | |
| 14 | + * IoT 设备 Modbus 点位配置 Service 接口 | |
| 15 | + * | |
| 16 | + * @author 芋道源码 | |
| 17 | + */ | |
| 18 | +public interface IotDeviceModbusPointService { | |
| 19 | + | |
| 20 | + /** | |
| 21 | + * 创建设备 Modbus 点位配置 | |
| 22 | + * | |
| 23 | + * @param createReqVO 创建信息 | |
| 24 | + * @return 编号 | |
| 25 | + */ | |
| 26 | + Long createDeviceModbusPoint(@Valid IotDeviceModbusPointSaveReqVO createReqVO); | |
| 27 | + | |
| 28 | + /** | |
| 29 | + * 更新设备 Modbus 点位配置 | |
| 30 | + * | |
| 31 | + * @param updateReqVO 更新信息 | |
| 32 | + */ | |
| 33 | + void updateDeviceModbusPoint(@Valid IotDeviceModbusPointSaveReqVO updateReqVO); | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * 删除设备 Modbus 点位配置 | |
| 37 | + * | |
| 38 | + * @param id 编号 | |
| 39 | + */ | |
| 40 | + void deleteDeviceModbusPoint(Long id); | |
| 41 | + | |
| 42 | + /** | |
| 43 | + * 获得设备 Modbus 点位配置 | |
| 44 | + * | |
| 45 | + * @param id 编号 | |
| 46 | + * @return 设备 Modbus 点位配置 | |
| 47 | + */ | |
| 48 | + IotDeviceModbusPointDO getDeviceModbusPoint(Long id); | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * 获得设备 Modbus 点位配置分页 | |
| 52 | + * | |
| 53 | + * @param pageReqVO 分页查询 | |
| 54 | + * @return 设备 Modbus 点位配置分页 | |
| 55 | + */ | |
| 56 | + PageResult<IotDeviceModbusPointDO> getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO); | |
| 57 | + | |
| 58 | + /** | |
| 59 | + * 物模型变更时,更新关联点位的冗余字段(identifier、name) | |
| 60 | + * | |
| 61 | + * @param thingModelId 物模型编号 | |
| 62 | + * @param identifier 物模型标识符 | |
| 63 | + * @param name 物模型名称 | |
| 64 | + */ | |
| 65 | + void updateDeviceModbusPointByThingModel(Long thingModelId, String identifier, String name); | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * 根据设备编号批量获得启用的点位配置 Map | |
| 69 | + * | |
| 70 | + * @param deviceIds 设备编号集合 | |
| 71 | + * @return 设备点位 Map,key 为设备编号,value 为点位配置列表 | |
| 72 | + */ | |
| 73 | + Map<Long, List<IotDeviceModbusPointDO>> getEnabledDeviceModbusPointMapByDeviceIds(Collection<Long> deviceIds); | |
| 74 | + | |
| 75 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.service.device; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.util.ObjUtil; | |
| 5 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | |
| 6 | +import cn.iocoder.yudao.framework.common.pojo.PageResult; | |
| 7 | +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | |
| 8 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; | |
| 9 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO; | |
| 10 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; | |
| 11 | +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; | |
| 12 | +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusPointMapper; | |
| 13 | +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; | |
| 14 | +import jakarta.annotation.Resource; | |
| 15 | +import org.springframework.stereotype.Service; | |
| 16 | +import org.springframework.validation.annotation.Validated; | |
| 17 | + | |
| 18 | +import java.util.Collection; | |
| 19 | +import java.util.Collections; | |
| 20 | +import java.util.List; | |
| 21 | +import java.util.Map; | |
| 22 | + | |
| 23 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 24 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; | |
| 25 | +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; | |
| 26 | + | |
| 27 | +/** | |
| 28 | + * IoT 设备 Modbus 点位配置 Service 实现类 | |
| 29 | + * | |
| 30 | + * @author 芋道源码 | |
| 31 | + */ | |
| 32 | +@Service | |
| 33 | +@Validated | |
| 34 | +public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointService { | |
| 35 | + | |
| 36 | + @Resource | |
| 37 | + private IotDeviceModbusPointMapper modbusPointMapper; | |
| 38 | + | |
| 39 | + @Resource | |
| 40 | + private IotDeviceService deviceService; | |
| 41 | + | |
| 42 | + @Resource | |
| 43 | + private IotThingModelService thingModelService; | |
| 44 | + | |
| 45 | + @Override | |
| 46 | + public Long createDeviceModbusPoint(IotDeviceModbusPointSaveReqVO createReqVO) { | |
| 47 | + // 1.1 校验设备存在 | |
| 48 | + deviceService.validateDeviceExists(createReqVO.getDeviceId()); | |
| 49 | + // 1.2 校验物模型属性存在 | |
| 50 | + IotThingModelDO thingModel = validateThingModelExists(createReqVO.getThingModelId()); | |
| 51 | + // 1.3 校验同一设备下点位唯一性(基于 identifier) | |
| 52 | + validateDeviceModbusPointUnique(createReqVO.getDeviceId(), thingModel.getIdentifier(), null); | |
| 53 | + | |
| 54 | + // 2. 插入 | |
| 55 | + IotDeviceModbusPointDO modbusPoint = BeanUtils.toBean(createReqVO, IotDeviceModbusPointDO.class, | |
| 56 | + o -> o.setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName())); | |
| 57 | + modbusPointMapper.insert(modbusPoint); | |
| 58 | + return modbusPoint.getId(); | |
| 59 | + } | |
| 60 | + | |
| 61 | + @Override | |
| 62 | + public void updateDeviceModbusPoint(IotDeviceModbusPointSaveReqVO updateReqVO) { | |
| 63 | + // 1.1 校验存在 | |
| 64 | + validateDeviceModbusPointExists(updateReqVO.getId()); | |
| 65 | + // 1.2 校验设备存在 | |
| 66 | + deviceService.validateDeviceExists(updateReqVO.getDeviceId()); | |
| 67 | + // 1.3 校验物模型属性存在 | |
| 68 | + IotThingModelDO thingModel = validateThingModelExists(updateReqVO.getThingModelId()); | |
| 69 | + // 1.4 校验同一设备下点位唯一性 | |
| 70 | + validateDeviceModbusPointUnique(updateReqVO.getDeviceId(), thingModel.getIdentifier(), updateReqVO.getId()); | |
| 71 | + | |
| 72 | + // 2. 更新 | |
| 73 | + IotDeviceModbusPointDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusPointDO.class, | |
| 74 | + o -> o.setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName())); | |
| 75 | + modbusPointMapper.updateById(updateObj); | |
| 76 | + } | |
| 77 | + | |
| 78 | + @Override | |
| 79 | + public void updateDeviceModbusPointByThingModel(Long thingModelId, String identifier, String name) { | |
| 80 | + IotDeviceModbusPointDO updateObj = new IotDeviceModbusPointDO() | |
| 81 | + .setIdentifier(identifier).setName(name); | |
| 82 | + modbusPointMapper.updateByThingModelId(thingModelId, updateObj); | |
| 83 | + } | |
| 84 | + | |
| 85 | + private IotThingModelDO validateThingModelExists(Long id) { | |
| 86 | + IotThingModelDO thingModel = thingModelService.getThingModel(id); | |
| 87 | + if (thingModel == null) { | |
| 88 | + throw exception(THING_MODEL_NOT_EXISTS); | |
| 89 | + } | |
| 90 | + return thingModel; | |
| 91 | + } | |
| 92 | + | |
| 93 | + @Override | |
| 94 | + public void deleteDeviceModbusPoint(Long id) { | |
| 95 | + // 校验存在 | |
| 96 | + validateDeviceModbusPointExists(id); | |
| 97 | + // 删除 | |
| 98 | + modbusPointMapper.deleteById(id); | |
| 99 | + } | |
| 100 | + | |
| 101 | + private void validateDeviceModbusPointExists(Long id) { | |
| 102 | + IotDeviceModbusPointDO point = modbusPointMapper.selectById(id); | |
| 103 | + if (point == null) { | |
| 104 | + throw exception(DEVICE_MODBUS_POINT_NOT_EXISTS); | |
| 105 | + } | |
| 106 | + } | |
| 107 | + | |
| 108 | + private void validateDeviceModbusPointUnique(Long deviceId, String identifier, Long excludeId) { | |
| 109 | + IotDeviceModbusPointDO point = modbusPointMapper.selectByDeviceIdAndIdentifier(deviceId, identifier); | |
| 110 | + if (point != null && ObjUtil.notEqual(point.getId(), excludeId)) { | |
| 111 | + throw exception(DEVICE_MODBUS_POINT_EXISTS); | |
| 112 | + } | |
| 113 | + } | |
| 114 | + | |
| 115 | + @Override | |
| 116 | + public IotDeviceModbusPointDO getDeviceModbusPoint(Long id) { | |
| 117 | + return modbusPointMapper.selectById(id); | |
| 118 | + } | |
| 119 | + | |
| 120 | + @Override | |
| 121 | + public PageResult<IotDeviceModbusPointDO> getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO) { | |
| 122 | + return modbusPointMapper.selectPage(pageReqVO); | |
| 123 | + } | |
| 124 | + | |
| 125 | + @Override | |
| 126 | + public Map<Long, List<IotDeviceModbusPointDO>> getEnabledDeviceModbusPointMapByDeviceIds(Collection<Long> deviceIds) { | |
| 127 | + if (CollUtil.isEmpty(deviceIds)) { | |
| 128 | + return Collections.emptyMap(); | |
| 129 | + } | |
| 130 | + List<IotDeviceModbusPointDO> pointList = modbusPointMapper.selectListByDeviceIdsAndStatus( | |
| 131 | + deviceIds, CommonStatusEnum.ENABLE.getStatus()); | |
| 132 | + return convertMultiMap(pointList, IotDeviceModbusPointDO::getDeviceId); | |
| 133 | + } | |
| 134 | + | |
| 135 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java
| ... | ... | @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; |
| 7 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | |
| 8 | 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
| ... | ... | @@ -17,7 +17,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; |
| 17 | 17 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 18 | 18 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; |
| 19 | 19 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 20 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 20 | +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | |
| 21 | 21 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 22 | 22 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 23 | 23 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordServiceImpl.java
| ... | ... | @@ -3,11 +3,15 @@ package cn.iocoder.yudao.module.iot.service.ota; |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.convert.Convert; |
| 5 | 5 | import cn.hutool.core.lang.Assert; |
| 6 | -import cn.hutool.core.map.MapUtil; | |
| 7 | 6 | import cn.hutool.core.util.StrUtil; |
| 8 | 7 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 8 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 9 | +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | |
| 9 | 10 | import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record.IotOtaTaskRecordPageReqVO; |
| 11 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 10 | 12 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 13 | +import cn.iocoder.yudao.module.iot.core.topic.ota.IotDeviceOtaProgressReqDTO; | |
| 14 | +import cn.iocoder.yudao.module.iot.core.topic.ota.IotDeviceOtaUpgradeReqDTO; | |
| 11 | 15 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 12 | 16 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; |
| 13 | 17 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; |
| ... | ... | @@ -133,9 +137,9 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService { |
| 133 | 137 | public boolean pushOtaTaskRecord(IotOtaTaskRecordDO record, IotOtaFirmwareDO fireware, IotDeviceDO device) { |
| 134 | 138 | try { |
| 135 | 139 | // 1. 推送 OTA 任务记录 |
| 136 | - IotDeviceMessage message = IotDeviceMessage.buildOtaUpgrade( | |
| 137 | - fireware.getVersion(), fireware.getFileUrl(), fireware.getFileSize(), | |
| 138 | - fireware.getFileDigestAlgorithm(), fireware.getFileDigestValue()); | |
| 140 | + IotDeviceOtaUpgradeReqDTO params = BeanUtils.toBean(fireware, IotDeviceOtaUpgradeReqDTO.class); | |
| 141 | + IotDeviceMessage message = IotDeviceMessage.requestOf( | |
| 142 | + IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), params); | |
| 139 | 143 | deviceMessageService.sendDeviceMessage(message, device); |
| 140 | 144 | |
| 141 | 145 | // 2. 更新 OTA 升级记录状态为进行中 |
| ... | ... | @@ -163,17 +167,16 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService { |
| 163 | 167 | |
| 164 | 168 | @Override |
| 165 | 169 | @Transactional(rollbackFor = Exception.class) |
| 166 | - @SuppressWarnings("unchecked") | |
| 167 | 170 | public void updateOtaRecordProgress(IotDeviceDO device, IotDeviceMessage message) { |
| 168 | 171 | // 1.1 参数解析 |
| 169 | - Map<String, Object> params = (Map<String, Object>) message.getParams(); | |
| 170 | - String version = MapUtil.getStr(params, "version"); | |
| 172 | + IotDeviceOtaProgressReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceOtaProgressReqDTO.class); | |
| 173 | + String version = params.getVersion(); | |
| 171 | 174 | Assert.notBlank(version, "version 不能为空"); |
| 172 | - Integer status = MapUtil.getInt(params, "status"); | |
| 175 | + Integer status = params.getStatus(); | |
| 173 | 176 | Assert.notNull(status, "status 不能为空"); |
| 174 | 177 | Assert.notNull(IotOtaTaskRecordStatusEnum.of(status), "status 状态不正确"); |
| 175 | - String description = MapUtil.getStr(params, "description"); | |
| 176 | - Integer progress = MapUtil.getInt(params, "progress"); | |
| 178 | + String description = params.getDescription(); | |
| 179 | + Integer progress = params.getProgress(); | |
| 177 | 180 | Assert.notNull(progress, "progress 不能为空"); |
| 178 | 181 | Assert.isTrue(progress >= 0 && progress <= 100, "progress 必须在 0-100 之间"); |
| 179 | 182 | // 1.2 查询 OTA 升级记录 | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java
| ... | ... | @@ -10,6 +10,9 @@ import javax.annotation.Nullable; |
| 10 | 10 | import java.time.LocalDateTime; |
| 11 | 11 | import java.util.Collection; |
| 12 | 12 | import java.util.List; |
| 13 | +import java.util.Map; | |
| 14 | + | |
| 15 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; | |
| 13 | 16 | |
| 14 | 17 | /** |
| 15 | 18 | * IoT 产品 Service 接口 |
| ... | ... | @@ -122,10 +125,35 @@ public interface IotProductService { |
| 122 | 125 | Long getProductCount(@Nullable LocalDateTime createTime); |
| 123 | 126 | |
| 124 | 127 | /** |
| 128 | + * 批量获得产品列表 | |
| 129 | + * | |
| 130 | + * @param ids 产品编号集合 | |
| 131 | + * @return 产品列表 | |
| 132 | + */ | |
| 133 | + List<IotProductDO> getProductList(Collection<Long> ids); | |
| 134 | + | |
| 135 | + /** | |
| 136 | + * 批量获得产品 Map | |
| 137 | + * | |
| 138 | + * @param ids 产品编号集合 | |
| 139 | + * @return 产品 Map(key: 产品编号, value: 产品) | |
| 140 | + */ | |
| 141 | + default Map<Long, IotProductDO> getProductMap(Collection<Long> ids) { | |
| 142 | + return convertMap(getProductList(ids), IotProductDO::getId); | |
| 143 | + } | |
| 144 | + | |
| 145 | + /** | |
| 125 | 146 | * 批量校验产品存在 |
| 126 | 147 | * |
| 127 | 148 | * @param ids 产品编号集合 |
| 128 | 149 | */ |
| 129 | 150 | void validateProductsExist(Collection<Long> ids); |
| 130 | 151 | |
| 152 | + /** | |
| 153 | + * 同步产品的 TDengine 表结构 | |
| 154 | + * | |
| 155 | + * 目的:当 MySQL 和 TDengine 不同步时,强制将已发布产品的表结构同步到 TDengine 中 | |
| 156 | + */ | |
| 157 | + void syncProductPropertyTable(); | |
| 158 | + | |
| 131 | 159 | } |
| 132 | 160 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java
| ... | ... | @@ -15,8 +15,10 @@ import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| 15 | 15 | import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; |
| 16 | 16 | import com.baomidou.dynamic.datasource.annotation.DSTransactional; |
| 17 | 17 | import jakarta.annotation.Resource; |
| 18 | +import lombok.extern.slf4j.Slf4j; | |
| 18 | 19 | import org.springframework.cache.annotation.CacheEvict; |
| 19 | 20 | import org.springframework.cache.annotation.Cacheable; |
| 21 | +import org.springframework.context.annotation.Lazy; | |
| 20 | 22 | import org.springframework.stereotype.Service; |
| 21 | 23 | import org.springframework.validation.annotation.Validated; |
| 22 | 24 | |
| ... | ... | @@ -33,6 +35,7 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; |
| 33 | 35 | * |
| 34 | 36 | * @author ahh |
| 35 | 37 | */ |
| 38 | +@Slf4j | |
| 36 | 39 | @Service |
| 37 | 40 | @Validated |
| 38 | 41 | public class IotProductServiceImpl implements IotProductService { |
| ... | ... | @@ -41,9 +44,10 @@ public class IotProductServiceImpl implements IotProductService { |
| 41 | 44 | private IotProductMapper productMapper; |
| 42 | 45 | |
| 43 | 46 | @Resource |
| 44 | - private IotDevicePropertyService devicePropertyDataService; | |
| 45 | - @Resource | |
| 46 | 47 | private IotDeviceService deviceService; |
| 48 | + @Resource | |
| 49 | + @Lazy // 延迟加载,避免循环依赖 | |
| 50 | + private IotDevicePropertyService devicePropertyDataService; | |
| 47 | 51 | |
| 48 | 52 | @Override |
| 49 | 53 | public Long createProduct(IotProductSaveReqVO createReqVO) { |
| ... | ... | @@ -172,6 +176,32 @@ public class IotProductServiceImpl implements IotProductService { |
| 172 | 176 | } |
| 173 | 177 | |
| 174 | 178 | @Override |
| 179 | + public List<IotProductDO> getProductList(Collection<Long> ids) { | |
| 180 | + return productMapper.selectByIds(ids); | |
| 181 | + } | |
| 182 | + | |
| 183 | + @Override | |
| 184 | + public void syncProductPropertyTable() { | |
| 185 | + // 1. 获取所有已发布的产品 | |
| 186 | + List<IotProductDO> products = productMapper.selectListByStatus( | |
| 187 | + IotProductStatusEnum.PUBLISHED.getStatus()); | |
| 188 | + log.info("[syncProductPropertyTable][开始同步,已发布产品数量({})]", products.size()); | |
| 189 | + | |
| 190 | + // 2. 遍历同步 TDengine 表结构(创建产品超级表数据模型) | |
| 191 | + int successCount = 0; | |
| 192 | + for (IotProductDO product : products) { | |
| 193 | + try { | |
| 194 | + devicePropertyDataService.defineDevicePropertyData(product.getId()); | |
| 195 | + successCount++; | |
| 196 | + log.info("[syncProductPropertyTable][产品({}/{}) 同步成功]", product.getId(), product.getName()); | |
| 197 | + } catch (Exception e) { | |
| 198 | + log.error("[syncProductPropertyTable][产品({}/{}) 同步失败]", product.getId(), product.getName(), e); | |
| 199 | + } | |
| 200 | + } | |
| 201 | + log.info("[syncProductPropertyTable][同步完成,成功({}/{})个]", successCount, products.size()); | |
| 202 | + } | |
| 203 | + | |
| 204 | + @Override | |
| 175 | 205 | public void validateProductsExist(Collection<Long> ids) { |
| 176 | 206 | if (CollUtil.isEmpty(ids)) { |
| 177 | 207 | return; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDevicePropertySetSceneRuleAction.java
| ... | ... | @@ -15,7 +15,6 @@ import lombok.extern.slf4j.Slf4j; |
| 15 | 15 | import org.springframework.stereotype.Component; |
| 16 | 16 | |
| 17 | 17 | import java.util.List; |
| 18 | -import java.util.Map; | |
| 19 | 18 | |
| 20 | 19 | /** |
| 21 | 20 | * IoT 设备属性设置的 {@link IotSceneRuleAction} 实现类 |
| ... | ... | @@ -24,7 +23,7 @@ import java.util.Map; |
| 24 | 23 | */ |
| 25 | 24 | @Component |
| 26 | 25 | @Slf4j |
| 27 | -public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction { | |
| 26 | +public class IotDevicePropertySetSceneRuleAction implements IotSceneRuleAction { | |
| 28 | 27 | |
| 29 | 28 | @Resource |
| 30 | 29 | private IotDeviceService deviceService; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java
| ... | ... | @@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; |
| 15 | 15 | import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper; |
| 16 | 16 | import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; |
| 17 | 17 | import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; |
| 18 | +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; | |
| 18 | 19 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; |
| 19 | 20 | import jakarta.annotation.Resource; |
| 20 | 21 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -50,6 +51,9 @@ public class IotThingModelServiceImpl implements IotThingModelService { |
| 50 | 51 | @Resource |
| 51 | 52 | @Lazy // 延迟加载,解决循环依赖 |
| 52 | 53 | private IotProductService productService; |
| 54 | + @Resource | |
| 55 | + @Lazy // 延迟加载,解决循环依赖 | |
| 56 | + private IotDeviceModbusPointService deviceModbusPointService; | |
| 53 | 57 | |
| 54 | 58 | @Override |
| 55 | 59 | @Transactional(rollbackFor = Exception.class) |
| ... | ... | @@ -84,7 +88,11 @@ public class IotThingModelServiceImpl implements IotThingModelService { |
| 84 | 88 | IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO); |
| 85 | 89 | thingModelMapper.updateById(thingModel); |
| 86 | 90 | |
| 87 | - // 3. 删除缓存 | |
| 91 | + // 3. 同步更新 Modbus 点位的冗余字段(identifier、name) | |
| 92 | + deviceModbusPointService.updateDeviceModbusPointByThingModel( | |
| 93 | + updateReqVO.getId(), updateReqVO.getIdentifier(), updateReqVO.getName()); | |
| 94 | + | |
| 95 | + // 4. 删除缓存 | |
| 88 | 96 | deleteThingModelListCache(updateReqVO.getProductId()); |
| 89 | 97 | } |
| 90 | 98 | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java
| ... | ... | @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.service.rule.scene; |
| 3 | 3 | import cn.hutool.core.collection.ListUtil; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 5 | 5 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
| 6 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | |
| 7 | 7 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 4 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.biz; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 5 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; | |
| 6 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 7 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.dto.*; | |
| 8 | 5 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; |
| 9 | 6 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; |
| 10 | 7 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; |
| ... | ... | @@ -50,4 +47,12 @@ public interface IotDeviceCommonApi { |
| 50 | 47 | */ |
| 51 | 48 | CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO); |
| 52 | 49 | |
| 50 | + /** | |
| 51 | + * 获取 Modbus 设备配置列表 | |
| 52 | + * | |
| 53 | + * @param listReqDTO 查询参数 | |
| 54 | + * @return Modbus 设备配置列表 | |
| 55 | + */ | |
| 56 | + CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList(IotModbusDeviceConfigListReqDTO listReqDTO); | |
| 57 | + | |
| 53 | 58 | } | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigListReqDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.biz.dto; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | +import lombok.experimental.Accessors; | |
| 5 | + | |
| 6 | +import java.util.Set; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * IoT Modbus 设备配置列表查询 Request DTO | |
| 10 | + * | |
| 11 | + * @author 芋道源码 | |
| 12 | + */ | |
| 13 | +@Data | |
| 14 | +@Accessors(chain = true) | |
| 15 | +public class IotModbusDeviceConfigListReqDTO { | |
| 16 | + | |
| 17 | + /** | |
| 18 | + * 状态 | |
| 19 | + */ | |
| 20 | + private Integer status; | |
| 21 | + | |
| 22 | + /** | |
| 23 | + * 模式 | |
| 24 | + */ | |
| 25 | + private Integer mode; | |
| 26 | + | |
| 27 | + /** | |
| 28 | + * 协议类型 | |
| 29 | + */ | |
| 30 | + private String protocolType; | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 设备 ID 集合 | |
| 34 | + */ | |
| 35 | + private Set<Long> deviceIds; | |
| 36 | + | |
| 37 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.biz.dto; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | + | |
| 5 | +import java.util.List; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * IoT Modbus 设备配置 Response DTO | |
| 9 | + * | |
| 10 | + * @author 芋道源码 | |
| 11 | + */ | |
| 12 | +@Data | |
| 13 | +public class IotModbusDeviceConfigRespDTO { | |
| 14 | + | |
| 15 | + /** | |
| 16 | + * 设备编号 | |
| 17 | + */ | |
| 18 | + private Long deviceId; | |
| 19 | + /** | |
| 20 | + * 产品标识 | |
| 21 | + */ | |
| 22 | + private String productKey; | |
| 23 | + /** | |
| 24 | + * 设备名称 | |
| 25 | + */ | |
| 26 | + private String deviceName; | |
| 27 | + | |
| 28 | + // ========== Modbus 连接配置 ========== | |
| 29 | + | |
| 30 | + /** | |
| 31 | + * Modbus 服务器 IP 地址 | |
| 32 | + */ | |
| 33 | + private String ip; | |
| 34 | + /** | |
| 35 | + * Modbus 服务器端口 | |
| 36 | + */ | |
| 37 | + private Integer port; | |
| 38 | + /** | |
| 39 | + * 从站地址 | |
| 40 | + */ | |
| 41 | + private Integer slaveId; | |
| 42 | + /** | |
| 43 | + * 连接超时时间,单位:毫秒 | |
| 44 | + */ | |
| 45 | + private Integer timeout; | |
| 46 | + /** | |
| 47 | + * 重试间隔,单位:毫秒 | |
| 48 | + */ | |
| 49 | + private Integer retryInterval; | |
| 50 | + /** | |
| 51 | + * 模式 | |
| 52 | + */ | |
| 53 | + private Integer mode; | |
| 54 | + /** | |
| 55 | + * 数据帧格式 | |
| 56 | + */ | |
| 57 | + private Integer frameFormat; | |
| 58 | + | |
| 59 | + // ========== Modbus 点位配置 ========== | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * 点位列表 | |
| 63 | + */ | |
| 64 | + private List<IotModbusPointRespDTO> points; | |
| 65 | + | |
| 66 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.biz.dto; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum; | |
| 5 | +import lombok.Data; | |
| 6 | + | |
| 7 | +import java.math.BigDecimal; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT Modbus 点位配置 Response DTO | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Data | |
| 15 | +public class IotModbusPointRespDTO { | |
| 16 | + | |
| 17 | + /** | |
| 18 | + * 点位编号 | |
| 19 | + */ | |
| 20 | + private Long id; | |
| 21 | + /** | |
| 22 | + * 属性标识符(物模型的 identifier) | |
| 23 | + */ | |
| 24 | + private String identifier; | |
| 25 | + /** | |
| 26 | + * 属性名称(物模型的 name) | |
| 27 | + */ | |
| 28 | + private String name; | |
| 29 | + | |
| 30 | + // ========== Modbus 协议配置 ========== | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * Modbus 功能码 | |
| 34 | + * | |
| 35 | + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器) | |
| 36 | + */ | |
| 37 | + private Integer functionCode; | |
| 38 | + /** | |
| 39 | + * 寄存器起始地址 | |
| 40 | + */ | |
| 41 | + private Integer registerAddress; | |
| 42 | + /** | |
| 43 | + * 寄存器数量 | |
| 44 | + */ | |
| 45 | + private Integer registerCount; | |
| 46 | + /** | |
| 47 | + * 字节序 | |
| 48 | + * | |
| 49 | + * 枚举 {@link IotModbusByteOrderEnum} | |
| 50 | + */ | |
| 51 | + private String byteOrder; | |
| 52 | + /** | |
| 53 | + * 原始数据类型 | |
| 54 | + * | |
| 55 | + * 枚举 {@link IotModbusRawDataTypeEnum} | |
| 56 | + */ | |
| 57 | + private String rawDataType; | |
| 58 | + /** | |
| 59 | + * 缩放因子 | |
| 60 | + */ | |
| 61 | + private BigDecimal scale; | |
| 62 | + /** | |
| 63 | + * 轮询间隔(毫秒) | |
| 64 | + */ | |
| 65 | + private Integer pollInterval; | |
| 66 | + | |
| 67 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
| ... | ... | @@ -64,7 +64,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> { |
| 64 | 64 | // ========== OTA 固件 ========== |
| 65 | 65 | // 可参考:https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates |
| 66 | 66 | |
| 67 | - OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false), | |
| 67 | + OTA_UPGRADE("thing.ota.upgrade", "OTA 固件信息推送", false), | |
| 68 | 68 | OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true), |
| 69 | 69 | |
| 70 | 70 | ; | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java
| ... | ... | @@ -25,7 +25,8 @@ public enum IotProtocolTypeEnum implements ArrayValuable<String> { |
| 25 | 25 | MQTT("mqtt"), |
| 26 | 26 | EMQX("emqx"), |
| 27 | 27 | COAP("coap"), |
| 28 | - MODBUS_TCP("modbus_tcp"); | |
| 28 | + MODBUS_TCP_CLIENT("modbus_tcp_client"), | |
| 29 | + MODBUS_TCP_SERVER("modbus_tcp_server"); | |
| 29 | 30 | |
| 30 | 31 | public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new); |
| 31 | 32 | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceStateEnum.java renamed to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/device/IotDeviceStateEnum.java
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusByteOrderEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.enums.modbus; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; | |
| 4 | +import lombok.Getter; | |
| 5 | +import lombok.RequiredArgsConstructor; | |
| 6 | + | |
| 7 | +import java.util.Arrays; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT Modbus 字节序枚举 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Getter | |
| 15 | +@RequiredArgsConstructor | |
| 16 | +public enum IotModbusByteOrderEnum implements ArrayValuable<String> { | |
| 17 | + | |
| 18 | + AB("AB", "大端序(16位)", 2), | |
| 19 | + BA("BA", "小端序(16位)", 2), | |
| 20 | + ABCD("ABCD", "大端序(32位)", 4), | |
| 21 | + CDAB("CDAB", "大端字交换(32位)", 4), | |
| 22 | + DCBA("DCBA", "小端序(32位)", 4), | |
| 23 | + BADC("BADC", "小端字交换(32位)", 4); | |
| 24 | + | |
| 25 | + public static final String[] ARRAYS = Arrays.stream(values()) | |
| 26 | + .map(IotModbusByteOrderEnum::getOrder) | |
| 27 | + .toArray(String[]::new); | |
| 28 | + | |
| 29 | + /** | |
| 30 | + * 字节序 | |
| 31 | + */ | |
| 32 | + private final String order; | |
| 33 | + /** | |
| 34 | + * 名称 | |
| 35 | + */ | |
| 36 | + private final String name; | |
| 37 | + /** | |
| 38 | + * 字节数 | |
| 39 | + */ | |
| 40 | + private final Integer byteCount; | |
| 41 | + | |
| 42 | + @Override | |
| 43 | + public String[] array() { | |
| 44 | + return ARRAYS; | |
| 45 | + } | |
| 46 | + | |
| 47 | + public static IotModbusByteOrderEnum getByOrder(String order) { | |
| 48 | + return Arrays.stream(values()) | |
| 49 | + .filter(e -> e.getOrder().equals(order)) | |
| 50 | + .findFirst() | |
| 51 | + .orElse(null); | |
| 52 | + } | |
| 53 | + | |
| 54 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusFrameFormatEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.enums.modbus; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; | |
| 4 | +import lombok.Getter; | |
| 5 | +import lombok.RequiredArgsConstructor; | |
| 6 | + | |
| 7 | +import java.util.Arrays; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT Modbus 数据帧格式枚举 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Getter | |
| 15 | +@RequiredArgsConstructor | |
| 16 | +public enum IotModbusFrameFormatEnum implements ArrayValuable<Integer> { | |
| 17 | + | |
| 18 | + MODBUS_TCP(1), | |
| 19 | + MODBUS_RTU(2); | |
| 20 | + | |
| 21 | + public static final Integer[] ARRAYS = Arrays.stream(values()) | |
| 22 | + .map(IotModbusFrameFormatEnum::getFormat) | |
| 23 | + .toArray(Integer[]::new); | |
| 24 | + | |
| 25 | + /** | |
| 26 | + * 格式 | |
| 27 | + */ | |
| 28 | + private final Integer format; | |
| 29 | + | |
| 30 | + @Override | |
| 31 | + public Integer[] array() { | |
| 32 | + return ARRAYS; | |
| 33 | + } | |
| 34 | + | |
| 35 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusModeEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.enums.modbus; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; | |
| 4 | +import lombok.Getter; | |
| 5 | +import lombok.RequiredArgsConstructor; | |
| 6 | + | |
| 7 | +import java.util.Arrays; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT Modbus 工作模式枚举 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Getter | |
| 15 | +@RequiredArgsConstructor | |
| 16 | +public enum IotModbusModeEnum implements ArrayValuable<Integer> { | |
| 17 | + | |
| 18 | + POLLING(1, "云端轮询"), | |
| 19 | + ACTIVE_REPORT(2, "边缘采集"); | |
| 20 | + | |
| 21 | + public static final Integer[] ARRAYS = Arrays.stream(values()) | |
| 22 | + .map(IotModbusModeEnum::getMode) | |
| 23 | + .toArray(Integer[]::new); | |
| 24 | + | |
| 25 | + /** | |
| 26 | + * 工作模式 | |
| 27 | + */ | |
| 28 | + private final Integer mode; | |
| 29 | + /** | |
| 30 | + * 模式名称 | |
| 31 | + */ | |
| 32 | + private final String name; | |
| 33 | + | |
| 34 | + @Override | |
| 35 | + public Integer[] array() { | |
| 36 | + return ARRAYS; | |
| 37 | + } | |
| 38 | + | |
| 39 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusRawDataTypeEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.enums.modbus; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; | |
| 4 | +import lombok.Getter; | |
| 5 | +import lombok.RequiredArgsConstructor; | |
| 6 | + | |
| 7 | +import java.util.Arrays; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT Modbus 原始数据类型枚举 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Getter | |
| 15 | +@RequiredArgsConstructor | |
| 16 | +public enum IotModbusRawDataTypeEnum implements ArrayValuable<String> { | |
| 17 | + | |
| 18 | + INT16("INT16", "有符号 16 位整数", 1), | |
| 19 | + UINT16("UINT16", "无符号 16 位整数", 1), | |
| 20 | + INT32("INT32", "有符号 32 位整数", 2), | |
| 21 | + UINT32("UINT32", "无符号 32 位整数", 2), | |
| 22 | + FLOAT("FLOAT", "32 位浮点数", 2), | |
| 23 | + DOUBLE("DOUBLE", "64 位浮点数", 4), | |
| 24 | + BOOLEAN("BOOLEAN", "布尔值(用于线圈)", 1), | |
| 25 | + STRING("STRING", "字符串", null); // null 表示可变长度 | |
| 26 | + | |
| 27 | + public static final String[] ARRAYS = Arrays.stream(values()) | |
| 28 | + .map(IotModbusRawDataTypeEnum::getType) | |
| 29 | + .toArray(String[]::new); | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 数据类型 | |
| 33 | + */ | |
| 34 | + private final String type; | |
| 35 | + /** | |
| 36 | + * 名称 | |
| 37 | + */ | |
| 38 | + private final String name; | |
| 39 | + /** | |
| 40 | + * 寄存器数量(null 表示可变) | |
| 41 | + */ | |
| 42 | + private final Integer registerCount; | |
| 43 | + | |
| 44 | + @Override | |
| 45 | + public String[] array() { | |
| 46 | + return ARRAYS; | |
| 47 | + } | |
| 48 | + | |
| 49 | + public static IotModbusRawDataTypeEnum getByType(String type) { | |
| 50 | + return Arrays.stream(values()) | |
| 51 | + .filter(e -> e.getType().equals(type)) | |
| 52 | + .findFirst() | |
| 53 | + .orElse(null); | |
| 54 | + } | |
| 55 | + | |
| 56 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.mq.message; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.map.MapUtil; | |
| 4 | 3 | import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; |
| 5 | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 6 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.topic.state.IotDeviceStateUpdateReqDTO; | |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 8 | 8 | import lombok.AllArgsConstructor; |
| 9 | 9 | import lombok.Builder; |
| ... | ... | @@ -72,7 +72,7 @@ public class IotDeviceMessage { |
| 72 | 72 | * 请求方法 |
| 73 | 73 | * |
| 74 | 74 | * 枚举 {@link IotDeviceMessageMethodEnum} |
| 75 | - * 例如说:thing.property.report 属性上报 | |
| 75 | + * 例如说:thing.property.post 属性上报 | |
| 76 | 76 | */ |
| 77 | 77 | private String method; |
| 78 | 78 | /** |
| ... | ... | @@ -149,20 +149,12 @@ public class IotDeviceMessage { |
| 149 | 149 | |
| 150 | 150 | public static IotDeviceMessage buildStateUpdateOnline() { |
| 151 | 151 | return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(), |
| 152 | - MapUtil.of("state", IotDeviceStateEnum.ONLINE.getState())); | |
| 152 | + new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.ONLINE.getState())); | |
| 153 | 153 | } |
| 154 | 154 | |
| 155 | 155 | public static IotDeviceMessage buildStateOffline() { |
| 156 | 156 | return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(), |
| 157 | - MapUtil.of("state", IotDeviceStateEnum.OFFLINE.getState())); | |
| 158 | - } | |
| 159 | - | |
| 160 | - public static IotDeviceMessage buildOtaUpgrade(String version, String fileUrl, Long fileSize, | |
| 161 | - String fileDigestAlgorithm, String fileDigestValue) { | |
| 162 | - return requestOf(IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), MapUtil.builder() | |
| 163 | - .put("version", version).put("fileUrl", fileUrl).put("fileSize", fileSize) | |
| 164 | - .put("fileDigestAlgorithm", fileDigestAlgorithm).put("fileDigestValue", fileDigestValue) | |
| 165 | - .build()); | |
| 157 | + new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.OFFLINE.getState())); | |
| 166 | 158 | } |
| 167 | 159 | |
| 168 | 160 | } | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.topic.auth; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 3 | 4 | import jakarta.validation.constraints.NotEmpty; |
| 4 | 5 | import lombok.Data; |
| 5 | 6 | |
| 6 | 7 | /** |
| 7 | 8 | * IoT 设备动态注册 Request DTO |
| 8 | 9 | * <p> |
| 9 | - * 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret | |
| 10 | + * 用于 {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} 消息的 params 参数 | |
| 11 | + * <p> | |
| 12 | + * 直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret | |
| 10 | 13 | * |
| 11 | 14 | * @author 芋道源码 |
| 12 | 15 | * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.topic.auth; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 3 | 4 | import lombok.AllArgsConstructor; |
| 4 | 5 | import lombok.Data; |
| 5 | 6 | import lombok.NoArgsConstructor; |
| ... | ... | @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; |
| 7 | 8 | /** |
| 8 | 9 | * IoT 设备动态注册 Response DTO |
| 9 | 10 | * <p> |
| 10 | - * 用于直连设备/网关的一型一密动态注册响应 | |
| 11 | + * 用于 {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} 响应的设备信息 | |
| 11 | 12 | * |
| 12 | 13 | * @author 芋道源码 |
| 13 | 14 | * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.topic.auth; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 3 | 4 | import jakarta.validation.constraints.NotEmpty; |
| 4 | 5 | import lombok.Data; |
| 5 | 6 | |
| 6 | 7 | /** |
| 7 | 8 | * IoT 子设备动态注册 Request DTO |
| 8 | 9 | * <p> |
| 9 | - * 用于 thing.auth.register.sub 消息的 params 数组元素 | |
| 10 | - * | |
| 10 | + * 用于 {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} 消息的 params 数组元素 | |
| 11 | + * <p> | |
| 11 | 12 | * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。 |
| 12 | 13 | * |
| 13 | 14 | * @author 芋道源码 | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.topic.auth; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 3 | 4 | import lombok.AllArgsConstructor; |
| 4 | 5 | import lombok.Data; |
| 5 | 6 | import lombok.NoArgsConstructor; |
| ... | ... | @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; |
| 7 | 8 | /** |
| 8 | 9 | * IoT 子设备动态注册 Response DTO |
| 9 | 10 | * <p> |
| 10 | - * 用于 thing.auth.register.sub 响应的设备信息 | |
| 11 | + * 用于 {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} 响应的设备信息 | |
| 11 | 12 | * |
| 12 | 13 | * @author 芋道源码 |
| 13 | 14 | * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a> | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/config/IotDeviceConfigPushReqDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.topic.config; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 4 | +import lombok.AllArgsConstructor; | |
| 5 | +import lombok.Data; | |
| 6 | +import lombok.NoArgsConstructor; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * IoT 设备配置推送 Request DTO | |
| 10 | + * <p> | |
| 11 | + * 用于 {@link IotDeviceMessageMethodEnum#CONFIG_PUSH} 下行消息的 params 参数 | |
| 12 | + * | |
| 13 | + * @author 芋道源码 | |
| 14 | + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1">阿里云 - 远程配置</a> | |
| 15 | + */ | |
| 16 | +@Data | |
| 17 | +@NoArgsConstructor | |
| 18 | +@AllArgsConstructor | |
| 19 | +public class IotDeviceConfigPushReqDTO { | |
| 20 | + | |
| 21 | + /** | |
| 22 | + * 配置编号 | |
| 23 | + */ | |
| 24 | + private String configId; | |
| 25 | + | |
| 26 | + /** | |
| 27 | + * 配置文件大小(字节) | |
| 28 | + */ | |
| 29 | + private Long configSize; | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 签名方法 | |
| 33 | + */ | |
| 34 | + private String signMethod; | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * 签名 | |
| 38 | + */ | |
| 39 | + private String sign; | |
| 40 | + | |
| 41 | + /** | |
| 42 | + * 配置文件下载地址 | |
| 43 | + */ | |
| 44 | + private String url; | |
| 45 | + | |
| 46 | + /** | |
| 47 | + * 获取类型 | |
| 48 | + * <p> | |
| 49 | + * file: 文件 | |
| 50 | + * content: 内容 | |
| 51 | + */ | |
| 52 | + private String getType; | |
| 53 | + | |
| 54 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.topic.event; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 3 | 4 | import lombok.Data; |
| 4 | 5 | |
| 5 | 6 | /** |
| 6 | 7 | * IoT 设备事件上报 Request DTO |
| 7 | 8 | * <p> |
| 8 | - * 用于 thing.event.post 消息的 params 参数 | |
| 9 | + * 用于 {@link IotDeviceMessageMethodEnum#EVENT_POST} 消息的 params 参数 | |
| 9 | 10 | * |
| 10 | 11 | * @author 芋道源码 |
| 11 | 12 | * @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-events">阿里云 - 设备上报事件</a> | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.topic.ota; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 4 | +import lombok.AllArgsConstructor; | |
| 5 | +import lombok.Data; | |
| 6 | +import lombok.NoArgsConstructor; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * IoT 设备 OTA 升级进度上报 Request DTO | |
| 10 | + * <p> | |
| 11 | + * 用于 {@link IotDeviceMessageMethodEnum#OTA_PROGRESS} 上行消息的 params 参数 | |
| 12 | + * | |
| 13 | + * @author 芋道源码 | |
| 14 | + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates">阿里云 - OTA 升级</a> | |
| 15 | + */ | |
| 16 | +@Data | |
| 17 | +@NoArgsConstructor | |
| 18 | +@AllArgsConstructor | |
| 19 | +public class IotDeviceOtaProgressReqDTO { | |
| 20 | + | |
| 21 | + /** | |
| 22 | + * 固件版本号 | |
| 23 | + */ | |
| 24 | + private String version; | |
| 25 | + | |
| 26 | + /** | |
| 27 | + * 升级状态 | |
| 28 | + */ | |
| 29 | + private Integer status; | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 描述信息 | |
| 33 | + */ | |
| 34 | + private String description; | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * 升级进度(0-100) | |
| 38 | + */ | |
| 39 | + private Integer progress; | |
| 40 | + | |
| 41 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.topic.ota; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 4 | +import lombok.AllArgsConstructor; | |
| 5 | +import lombok.Data; | |
| 6 | +import lombok.NoArgsConstructor; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * IoT 设备 OTA 固件升级推送 Request DTO | |
| 10 | + * <p> | |
| 11 | + * 用于 {@link IotDeviceMessageMethodEnum#OTA_UPGRADE} 下行消息的 params 参数 | |
| 12 | + * | |
| 13 | + * @author 芋道源码 | |
| 14 | + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates">阿里云 - OTA 升级</a> | |
| 15 | + */ | |
| 16 | +@Data | |
| 17 | +@NoArgsConstructor | |
| 18 | +@AllArgsConstructor | |
| 19 | +public class IotDeviceOtaUpgradeReqDTO { | |
| 20 | + | |
| 21 | + /** | |
| 22 | + * 固件版本号 | |
| 23 | + */ | |
| 24 | + private String version; | |
| 25 | + | |
| 26 | + /** | |
| 27 | + * 固件文件下载地址 | |
| 28 | + */ | |
| 29 | + private String fileUrl; | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 固件文件大小(字节) | |
| 33 | + */ | |
| 34 | + private Long fileSize; | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * 固件文件摘要算法 | |
| 38 | + */ | |
| 39 | + private String fileDigestAlgorithm; | |
| 40 | + | |
| 41 | + /** | |
| 42 | + * 固件文件摘要值 | |
| 43 | + */ | |
| 44 | + private String fileDigestValue; | |
| 45 | + | |
| 46 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.topic.property; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 3 | 4 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 4 | 5 | import lombok.Data; |
| 5 | 6 | |
| ... | ... | @@ -9,7 +10,7 @@ import java.util.Map; |
| 9 | 10 | /** |
| 10 | 11 | * IoT 设备属性批量上报 Request DTO |
| 11 | 12 | * <p> |
| 12 | - * 用于 thing.event.property.pack.post 消息的 params 参数 | |
| 13 | + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_PACK_POST} 消息的 params 参数 | |
| 13 | 14 | * |
| 14 | 15 | * @author 芋道源码 |
| 15 | 16 | * @see <a href="http://help.aliyun.com/zh/marketplace/gateway-reports-data-in-batches">阿里云 - 网关批量上报数据</a> | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.topic.property; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 4 | + | |
| 3 | 5 | import java.util.HashMap; |
| 4 | 6 | import java.util.Map; |
| 5 | 7 | |
| 6 | 8 | /** |
| 7 | 9 | * IoT 设备属性上报 Request DTO |
| 8 | 10 | * <p> |
| 9 | - * 用于 thing.property.post 消息的 params 参数 | |
| 11 | + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_POST} 消息的 params 参数 | |
| 10 | 12 | * <p> |
| 11 | 13 | * 本质是一个 Map,key 为属性标识符,value 为属性值 |
| 12 | 14 | * | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertySetReqDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.topic.property; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 4 | + | |
| 5 | +import java.util.HashMap; | |
| 6 | +import java.util.Map; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * IoT 设备属性设置 Request DTO | |
| 10 | + * <p> | |
| 11 | + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_SET} 下行消息的 params 参数 | |
| 12 | + * <p> | |
| 13 | + * 本质是一个 Map,key 为属性标识符,value 为属性值 | |
| 14 | + * | |
| 15 | + * @author 芋道源码 | |
| 16 | + */ | |
| 17 | +public class IotDevicePropertySetReqDTO extends HashMap<String, Object> { | |
| 18 | + | |
| 19 | + public IotDevicePropertySetReqDTO() { | |
| 20 | + super(); | |
| 21 | + } | |
| 22 | + | |
| 23 | + public IotDevicePropertySetReqDTO(Map<String, Object> properties) { | |
| 24 | + super(properties); | |
| 25 | + } | |
| 26 | + | |
| 27 | + /** | |
| 28 | + * 创建属性设置 DTO | |
| 29 | + * | |
| 30 | + * @param properties 属性数据 | |
| 31 | + * @return DTO 对象 | |
| 32 | + */ | |
| 33 | + public static IotDevicePropertySetReqDTO of(Map<String, Object> properties) { | |
| 34 | + return new IotDevicePropertySetReqDTO(properties); | |
| 35 | + } | |
| 36 | + | |
| 37 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/service/IotDeviceServiceInvokeReqDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.topic.service; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 4 | +import lombok.AllArgsConstructor; | |
| 5 | +import lombok.Data; | |
| 6 | +import lombok.NoArgsConstructor; | |
| 7 | + | |
| 8 | +import java.util.Map; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * IoT 设备服务调用 Request DTO | |
| 12 | + * <p> | |
| 13 | + * 用于 {@link IotDeviceMessageMethodEnum#SERVICE_INVOKE} 下行消息的 params 参数 | |
| 14 | + * | |
| 15 | + * @author 芋道源码 | |
| 16 | + */ | |
| 17 | +@Data | |
| 18 | +@NoArgsConstructor | |
| 19 | +@AllArgsConstructor | |
| 20 | +public class IotDeviceServiceInvokeReqDTO { | |
| 21 | + | |
| 22 | + /** | |
| 23 | + * 服务标识符 | |
| 24 | + */ | |
| 25 | + private String identifier; | |
| 26 | + | |
| 27 | + /** | |
| 28 | + * 服务输入参数 | |
| 29 | + */ | |
| 30 | + private Map<String, Object> inputParams; | |
| 31 | + | |
| 32 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.topic.state; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 4 | +import lombok.AllArgsConstructor; | |
| 5 | +import lombok.Data; | |
| 6 | +import lombok.NoArgsConstructor; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * IoT 设备状态更新 Request DTO | |
| 10 | + * <p> | |
| 11 | + * 用于 {@link IotDeviceMessageMethodEnum#STATE_UPDATE} 消息的 params 参数 | |
| 12 | + * | |
| 13 | + * @author 芋道源码 | |
| 14 | + */ | |
| 15 | +@Data | |
| 16 | +@NoArgsConstructor | |
| 17 | +@AllArgsConstructor | |
| 18 | +public class IotDeviceStateUpdateReqDTO { | |
| 19 | + | |
| 20 | + /** | |
| 21 | + * 设备状态 | |
| 22 | + */ | |
| 23 | + private Integer state; | |
| 24 | + | |
| 25 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.topic.topo; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 4 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 4 | 5 | import jakarta.validation.constraints.NotEmpty; |
| 5 | 6 | import lombok.Data; |
| 6 | 7 | |
| ... | ... | @@ -9,7 +10,7 @@ import java.util.List; |
| 9 | 10 | /** |
| 10 | 11 | * IoT 设备拓扑添加 Request DTO |
| 11 | 12 | * <p> |
| 12 | - * 用于 thing.topo.add 消息的 params 参数 | |
| 13 | + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_ADD} 消息的 params 参数 | |
| 13 | 14 | * |
| 14 | 15 | * @author 芋道源码 |
| 15 | 16 | * @see <a href="http://help.aliyun.com/zh/marketplace/add-topological-relationship">阿里云 - 添加拓扑关系</a> | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.topic.topo; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 3 | 4 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 4 | 5 | import lombok.AllArgsConstructor; |
| 5 | 6 | import lombok.Data; |
| ... | ... | @@ -10,7 +11,7 @@ import java.util.List; |
| 10 | 11 | /** |
| 11 | 12 | * IoT 设备拓扑关系变更通知 Request DTO |
| 12 | 13 | * <p> |
| 13 | - * 用于 thing.topo.change 下行消息的 params 参数 | |
| 14 | + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_CHANGE} 下行消息的 params 参数 | |
| 14 | 15 | * |
| 15 | 16 | * @author 芋道源码 |
| 16 | 17 | * @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a> | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.topic.topo; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 3 | 4 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 4 | 5 | import jakarta.validation.Valid; |
| 5 | 6 | import jakarta.validation.constraints.NotEmpty; |
| ... | ... | @@ -10,7 +11,7 @@ import java.util.List; |
| 10 | 11 | /** |
| 11 | 12 | * IoT 设备拓扑删除 Request DTO |
| 12 | 13 | * <p> |
| 13 | - * 用于 thing.topo.delete 消息的 params 参数 | |
| 14 | + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_DELETE} 消息的 params 参数 | |
| 14 | 15 | * |
| 15 | 16 | * @author 芋道源码 |
| 16 | 17 | * @see <a href="https://help.aliyun.com/zh/marketplace/delete-a-topological-relationship">阿里云 - 删除拓扑关系</a> | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.topic.topo; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 3 | 4 | import lombok.Data; |
| 4 | 5 | |
| 5 | 6 | /** |
| 6 | 7 | * IoT 设备拓扑关系获取 Request DTO |
| 7 | 8 | * <p> |
| 8 | - * 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展) | |
| 9 | + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_GET} 请求的 params 参数(目前为空,预留扩展) | |
| 9 | 10 | * |
| 10 | 11 | * @author 芋道源码 |
| 11 | 12 | * @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a> | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.topic.topo; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 3 | 4 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 4 | 5 | import lombok.Data; |
| 5 | 6 | |
| ... | ... | @@ -8,7 +9,7 @@ import java.util.List; |
| 8 | 9 | /** |
| 9 | 10 | * IoT 设备拓扑关系获取 Response DTO |
| 10 | 11 | * <p> |
| 11 | - * 用于 thing.topo.get 响应 | |
| 12 | + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_GET} 响应 | |
| 12 | 13 | * |
| 13 | 14 | * @author 芋道源码 |
| 14 | 15 | * @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a> | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
| ... | ... | @@ -25,6 +25,14 @@ public class IotDeviceAuthUtils { |
| 25 | 25 | return String.format("%s.%s", productKey, deviceName); |
| 26 | 26 | } |
| 27 | 27 | |
| 28 | + public static String buildClientIdFromUsername(String username) { | |
| 29 | + IotDeviceIdentity identity = parseUsername(username); | |
| 30 | + if (identity == null) { | |
| 31 | + return null; | |
| 32 | + } | |
| 33 | + return buildClientId(identity.getProductKey(), identity.getDeviceName()); | |
| 34 | + } | |
| 35 | + | |
| 28 | 36 | public static String buildUsername(String productKey, String deviceName) { |
| 29 | 37 | return String.format("%s&%s", deviceName, productKey); |
| 30 | 38 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/pom.xml
| ... | ... | @@ -33,7 +33,7 @@ |
| 33 | 33 | <groupId>org.apache.rocketmq</groupId> |
| 34 | 34 | <artifactId>rocketmq-spring-boot-starter</artifactId> |
| 35 | 35 | <!-- TODO @芋艿:消息队列,后续可能去掉,默认不使用 rocketmq --> |
| 36 | -<!-- <optional>true</optional> --> | |
| 36 | + <optional>true</optional> | |
| 37 | 37 | </dependency> |
| 38 | 38 | |
| 39 | 39 | <!-- 工具类相关 --> |
| ... | ... | @@ -48,6 +48,13 @@ |
| 48 | 48 | <artifactId>vertx-mqtt</artifactId> |
| 49 | 49 | </dependency> |
| 50 | 50 | |
| 51 | + <!-- Modbus 相关 --> | |
| 52 | + <dependency> | |
| 53 | + <groupId>com.ghgande</groupId> | |
| 54 | + <artifactId>j2mod</artifactId> | |
| 55 | + <version>3.2.1</version> | |
| 56 | + </dependency> | |
| 57 | + | |
| 51 | 58 | <!-- CoAP 相关 - Eclipse Californium --> |
| 52 | 59 | <dependency> |
| 53 | 60 | <groupId>org.eclipse.californium</groupId> | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| ... | ... | @@ -6,6 +6,11 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties |
| 6 | 6 | import org.springframework.context.annotation.Bean; |
| 7 | 7 | import org.springframework.context.annotation.Configuration; |
| 8 | 8 | |
| 9 | +/** | |
| 10 | + * IoT 网关配置类 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 9 | 14 | @Configuration |
| 10 | 15 | @EnableConfigurationProperties(IotGatewayProperties.class) |
| 11 | 16 | public class IotGatewayConfiguration { | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| ... | ... | @@ -4,6 +4,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientConfig; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerConfig; | |
| 7 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; |
| 8 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; |
| 9 | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; |
| ... | ... | @@ -166,6 +168,18 @@ public class IotGatewayProperties { |
| 166 | 168 | @Valid |
| 167 | 169 | private IotEmqxConfig emqx; |
| 168 | 170 | |
| 171 | + /** | |
| 172 | + * Modbus TCP Client 协议配置 | |
| 173 | + */ | |
| 174 | + @Valid | |
| 175 | + private IotModbusTcpClientConfig modbusTcpClient; | |
| 176 | + | |
| 177 | + /** | |
| 178 | + * Modbus TCP Server 协议配置 | |
| 179 | + */ | |
| 180 | + @Valid | |
| 181 | + private IotModbusTcpServerConfig modbusTcpServer; | |
| 182 | + | |
| 169 | 183 | } |
| 170 | 184 | |
| 171 | 185 | /** | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/AbstractIotProtocolDownstreamSubscriber.java
| ... | ... | @@ -17,7 +17,7 @@ import lombok.extern.slf4j.Slf4j; |
| 17 | 17 | */ |
| 18 | 18 | @AllArgsConstructor |
| 19 | 19 | @Slf4j |
| 20 | -public abstract class IotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | |
| 20 | +public abstract class AbstractIotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | |
| 21 | 21 | |
| 22 | 22 | private final IotProtocol protocol; |
| 23 | 23 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java
| ... | ... | @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol; | |
| 10 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; |
| 11 | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; |
| 12 | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; |
| ... | ... | @@ -112,6 +114,10 @@ public class IotProtocolManager implements SmartLifecycle { |
| 112 | 114 | return createMqttProtocol(config); |
| 113 | 115 | case EMQX: |
| 114 | 116 | return createEmqxProtocol(config); |
| 117 | + case MODBUS_TCP_CLIENT: | |
| 118 | + return createModbusTcpClientProtocol(config); | |
| 119 | + case MODBUS_TCP_SERVER: | |
| 120 | + return createModbusTcpServerProtocol(config); | |
| 115 | 121 | default: |
| 116 | 122 | throw new IllegalArgumentException(String.format( |
| 117 | 123 | "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); |
| ... | ... | @@ -188,4 +194,24 @@ public class IotProtocolManager implements SmartLifecycle { |
| 188 | 194 | return new IotEmqxProtocol(config); |
| 189 | 195 | } |
| 190 | 196 | |
| 197 | + /** | |
| 198 | + * 创建 Modbus TCP Client 协议实例 | |
| 199 | + * | |
| 200 | + * @param config 协议实例配置 | |
| 201 | + * @return Modbus TCP Client 协议实例 | |
| 202 | + */ | |
| 203 | + private IotModbusTcpClientProtocol createModbusTcpClientProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 204 | + return new IotModbusTcpClientProtocol(config); | |
| 205 | + } | |
| 206 | + | |
| 207 | + /** | |
| 208 | + * 创建 Modbus TCP Server 协议实例 | |
| 209 | + * | |
| 210 | + * @param config 协议实例配置 | |
| 211 | + * @return Modbus TCP Server 协议实例 | |
| 212 | + */ | |
| 213 | + private IotModbusTcpServerProtocol createModbusTcpServerProtocol(IotGatewayProperties.ProtocolProperties config) { | |
| 214 | + return new IotModbusTcpServerProtocol(config); | |
| 215 | + } | |
| 216 | + | |
| 191 | 217 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java
| ... | ... | @@ -64,17 +64,13 @@ public class IotCoapProtocol implements IotProtocol { |
| 64 | 64 | /** |
| 65 | 65 | * 下行消息订阅者 |
| 66 | 66 | */ |
| 67 | - private final IotCoapDownstreamSubscriber downstreamSubscriber; | |
| 67 | + private IotCoapDownstreamSubscriber downstreamSubscriber; | |
| 68 | 68 | |
| 69 | 69 | public IotCoapProtocol(ProtocolProperties properties) { |
| 70 | 70 | IotCoapConfig coapConfig = properties.getCoap(); |
| 71 | 71 | Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); |
| 72 | 72 | this.properties = properties; |
| 73 | 73 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); |
| 74 | - | |
| 75 | - // 初始化下行消息订阅者 | |
| 76 | - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 77 | - this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus); | |
| 78 | 74 | } |
| 79 | 75 | |
| 80 | 76 | @Override |
| ... | ... | @@ -94,9 +90,9 @@ public class IotCoapProtocol implements IotProtocol { |
| 94 | 90 | return; |
| 95 | 91 | } |
| 96 | 92 | |
| 97 | - IotCoapConfig coapConfig = properties.getCoap(); | |
| 98 | 93 | try { |
| 99 | 94 | // 1.1 创建 CoAP 配置 |
| 95 | + IotCoapConfig coapConfig = properties.getCoap(); | |
| 100 | 96 | Configuration config = Configuration.createStandardWithoutFile(); |
| 101 | 97 | config.set(CoapConfig.COAP_PORT, properties.getPort()); |
| 102 | 98 | config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize()); |
| ... | ... | @@ -131,13 +127,12 @@ public class IotCoapProtocol implements IotProtocol { |
| 131 | 127 | getId(), properties.getPort(), serverId); |
| 132 | 128 | |
| 133 | 129 | // 4. 启动下行消息订阅者 |
| 130 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 131 | + this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus); | |
| 134 | 132 | this.downstreamSubscriber.start(); |
| 135 | 133 | } catch (Exception e) { |
| 136 | 134 | log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e); |
| 137 | - if (coapServer != null) { | |
| 138 | - coapServer.destroy(); | |
| 139 | - coapServer = null; | |
| 140 | - } | |
| 135 | + stop0(); | |
| 141 | 136 | throw e; |
| 142 | 137 | } |
| 143 | 138 | } |
| ... | ... | @@ -147,12 +142,19 @@ public class IotCoapProtocol implements IotProtocol { |
| 147 | 142 | if (!running) { |
| 148 | 143 | return; |
| 149 | 144 | } |
| 145 | + stop0(); | |
| 146 | + } | |
| 147 | + | |
| 148 | + private void stop0() { | |
| 150 | 149 | // 1. 停止下行消息订阅者 |
| 151 | - try { | |
| 152 | - downstreamSubscriber.stop(); | |
| 153 | - log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId()); | |
| 154 | - } catch (Exception e) { | |
| 155 | - log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 150 | + if (downstreamSubscriber != null) { | |
| 151 | + try { | |
| 152 | + downstreamSubscriber.stop(); | |
| 153 | + log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId()); | |
| 154 | + } catch (Exception e) { | |
| 155 | + log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 156 | + } | |
| 157 | + downstreamSubscriber = null; | |
| 156 | 158 | } |
| 157 | 159 | |
| 158 | 160 | // 2. 关闭 CoAP 服务器 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java
| ... | ... | @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; |
| 7 | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | |
| ... | ... | @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; |
| 12 | 12 | * @author 芋道源码 |
| 13 | 13 | */ |
| 14 | 14 | @Slf4j |
| 15 | -public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 15 | +public class IotCoapDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { | |
| 16 | 16 | |
| 17 | 17 | public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) { |
| 18 | 18 | super(protocol, messageBus); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java
| ... | ... | @@ -90,7 +90,7 @@ public class IotEmqxProtocol implements IotProtocol { |
| 90 | 90 | /** |
| 91 | 91 | * 下行消息订阅者 |
| 92 | 92 | */ |
| 93 | - private final IotEmqxDownstreamSubscriber downstreamSubscriber; | |
| 93 | + private IotEmqxDownstreamSubscriber downstreamSubscriber; | |
| 94 | 94 | |
| 95 | 95 | public IotEmqxProtocol(ProtocolProperties properties) { |
| 96 | 96 | Assert.notNull(properties, "协议实例配置不能为空"); |
| ... | ... | @@ -101,10 +101,6 @@ public class IotEmqxProtocol implements IotProtocol { |
| 101 | 101 | "MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空"); |
| 102 | 102 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); |
| 103 | 103 | this.upstreamHandler = new IotEmqxUpstreamHandler(serverId); |
| 104 | - | |
| 105 | - // 初始化下行消息订阅者 | |
| 106 | - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 107 | - this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus); | |
| 108 | 104 | } |
| 109 | 105 | |
| 110 | 106 | @Override |
| ... | ... | @@ -124,7 +120,7 @@ public class IotEmqxProtocol implements IotProtocol { |
| 124 | 120 | return; |
| 125 | 121 | } |
| 126 | 122 | |
| 127 | - // 1.1 创建 Vertx 实例 | |
| 123 | + // 1.1 创建 Vertx 实例 和 下行消息订阅者 | |
| 128 | 124 | this.vertx = Vertx.vertx(); |
| 129 | 125 | |
| 130 | 126 | try { |
| ... | ... | @@ -138,6 +134,8 @@ public class IotEmqxProtocol implements IotProtocol { |
| 138 | 134 | getId(), properties.getPort(), serverId); |
| 139 | 135 | |
| 140 | 136 | // 2. 启动下行消息订阅者 |
| 137 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 138 | + this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus); | |
| 141 | 139 | this.downstreamSubscriber.start(); |
| 142 | 140 | } catch (Exception e) { |
| 143 | 141 | log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e); |
| ... | ... | @@ -157,11 +155,14 @@ public class IotEmqxProtocol implements IotProtocol { |
| 157 | 155 | |
| 158 | 156 | private void stop0() { |
| 159 | 157 | // 1. 停止下行消息订阅者 |
| 160 | - try { | |
| 161 | - downstreamSubscriber.stop(); | |
| 162 | - log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId()); | |
| 163 | - } catch (Exception e) { | |
| 164 | - log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 158 | + if (downstreamSubscriber != null) { | |
| 159 | + try { | |
| 160 | + downstreamSubscriber.stop(); | |
| 161 | + log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId()); | |
| 162 | + } catch (Exception e) { | |
| 163 | + log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 164 | + } | |
| 165 | + downstreamSubscriber = null; | |
| 165 | 166 | } |
| 166 | 167 | |
| 167 | 168 | // 2.1 先置为 false:避免 closeHandler 触发重连 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java
| ... | ... | @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; |
| 7 | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | |
| ... | ... | @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; |
| 12 | 12 | * @author 芋道源码 |
| 13 | 13 | */ |
| 14 | 14 | @Slf4j |
| 15 | -public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 15 | +public class IotEmqxDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { | |
| 16 | 16 | |
| 17 | 17 | private final IotEmqxDownstreamHandler downstreamHandler; |
| 18 | 18 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java
| ... | ... | @@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil; |
| 5 | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 8 | 9 | import io.vertx.mqtt.messages.MqttPublishMessage; |
| 9 | 10 | import lombok.extern.slf4j.Slf4j; |
| 10 | 11 | |
| ... | ... | @@ -42,12 +43,14 @@ public class IotEmqxUpstreamHandler { |
| 42 | 43 | return; |
| 43 | 44 | } |
| 44 | 45 | |
| 45 | - // 2. 反序列化消息 | |
| 46 | + // 2.1 反序列化消息 | |
| 46 | 47 | IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); |
| 47 | 48 | if (message == null) { |
| 48 | 49 | log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); |
| 49 | 50 | return; |
| 50 | 51 | } |
| 52 | + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀) | |
| 53 | + IotMqttTopicUtils.normalizeReplyMethod(message); | |
| 51 | 54 | |
| 52 | 55 | // 3. 发送消息到队列 |
| 53 | 56 | deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java
| ... | ... | @@ -64,10 +64,6 @@ public class IotHttpProtocol implements IotProtocol { |
| 64 | 64 | public IotHttpProtocol(ProtocolProperties properties) { |
| 65 | 65 | this.properties = properties; |
| 66 | 66 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); |
| 67 | - | |
| 68 | - // 初始化下行消息订阅者 | |
| 69 | - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 70 | - this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); | |
| 71 | 67 | } |
| 72 | 68 | |
| 73 | 69 | @Override |
| ... | ... | @@ -87,7 +83,7 @@ public class IotHttpProtocol implements IotProtocol { |
| 87 | 83 | return; |
| 88 | 84 | } |
| 89 | 85 | |
| 90 | - // 1.1 创建 Vertx 实例(每个 Protocol 独立管理) | |
| 86 | + // 1.1 创建 Vertx 实例 | |
| 91 | 87 | this.vertx = Vertx.vertx(); |
| 92 | 88 | |
| 93 | 89 | // 1.2 创建路由 |
| ... | ... | @@ -123,18 +119,12 @@ public class IotHttpProtocol implements IotProtocol { |
| 123 | 119 | getId(), properties.getPort(), serverId); |
| 124 | 120 | |
| 125 | 121 | // 2. 启动下行消息订阅者 |
| 122 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 123 | + this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); | |
| 126 | 124 | this.downstreamSubscriber.start(); |
| 127 | 125 | } catch (Exception e) { |
| 128 | 126 | log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); |
| 129 | - // 启动失败时关闭资源 | |
| 130 | - if (httpServer != null) { | |
| 131 | - httpServer.close(); | |
| 132 | - httpServer = null; | |
| 133 | - } | |
| 134 | - if (vertx != null) { | |
| 135 | - vertx.close(); | |
| 136 | - vertx = null; | |
| 137 | - } | |
| 127 | + stop0(); | |
| 138 | 128 | throw e; |
| 139 | 129 | } |
| 140 | 130 | } |
| ... | ... | @@ -144,6 +134,10 @@ public class IotHttpProtocol implements IotProtocol { |
| 144 | 134 | if (!running) { |
| 145 | 135 | return; |
| 146 | 136 | } |
| 137 | + stop0(); | |
| 138 | + } | |
| 139 | + | |
| 140 | + private void stop0() { | |
| 147 | 141 | // 1. 停止下行消息订阅者 |
| 148 | 142 | if (downstreamSubscriber != null) { |
| 149 | 143 | try { | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java
| ... | ... | @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream; |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; | |
| 7 | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | |
| 9 | 9 | /** |
| ... | ... | @@ -13,7 +13,7 @@ import lombok.extern.slf4j.Slf4j; |
| 13 | 13 | */ |
| 14 | 14 | |
| 15 | 15 | @Slf4j |
| 16 | -public class IotHttpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 16 | +public class IotHttpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { | |
| 17 | 17 | |
| 18 | 18 | public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) { |
| 19 | 19 | super(protocol, messageBus); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | |
| 6 | +import io.vertx.core.Vertx; | |
| 7 | +import lombok.AllArgsConstructor; | |
| 8 | +import lombok.Data; | |
| 9 | +import lombok.extern.slf4j.Slf4j; | |
| 10 | + | |
| 11 | +import java.util.*; | |
| 12 | +import java.util.concurrent.ConcurrentHashMap; | |
| 13 | +import java.util.concurrent.ConcurrentLinkedQueue; | |
| 14 | + | |
| 15 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * Modbus 轮询调度器基类 | |
| 19 | + * <p> | |
| 20 | + * 封装通用的定时器管理、per-device 请求队列限速逻辑。 | |
| 21 | + * 子类只需实现 {@link #pollPoint(Long, Long)} 定义具体的轮询动作。 | |
| 22 | + * <p> | |
| 23 | + * | |
| 24 | + * @author 芋道源码 | |
| 25 | + */ | |
| 26 | +@Slf4j | |
| 27 | +public abstract class AbstractIotModbusPollScheduler { | |
| 28 | + | |
| 29 | + protected final Vertx vertx; | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积 | |
| 33 | + */ | |
| 34 | + private static final long MIN_REQUEST_INTERVAL = 1000; | |
| 35 | + /** | |
| 36 | + * 每个设备请求队列的最大长度,超出时丢弃最旧请求 | |
| 37 | + */ | |
| 38 | + private static final int MAX_QUEUE_SIZE = 1000; | |
| 39 | + | |
| 40 | + /** | |
| 41 | + * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) | |
| 42 | + */ | |
| 43 | + private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>(); | |
| 44 | + | |
| 45 | + /** | |
| 46 | + * per-device 请求队列:deviceId -> 待执行请求队列 | |
| 47 | + */ | |
| 48 | + private final Map<Long, Queue<Runnable>> deviceRequestQueues = new ConcurrentHashMap<>(); | |
| 49 | + /** | |
| 50 | + * per-device 上次请求时间戳:deviceId -> lastRequestTimeMs | |
| 51 | + */ | |
| 52 | + private final Map<Long, Long> deviceLastRequestTime = new ConcurrentHashMap<>(); | |
| 53 | + /** | |
| 54 | + * per-device 延迟 timer 标记:deviceId -> 是否有延迟 timer 在等待 | |
| 55 | + */ | |
| 56 | + private final Map<Long, Boolean> deviceDelayTimerActive = new ConcurrentHashMap<>(); | |
| 57 | + | |
| 58 | + protected AbstractIotModbusPollScheduler(Vertx vertx) { | |
| 59 | + this.vertx = vertx; | |
| 60 | + } | |
| 61 | + | |
| 62 | + /** | |
| 63 | + * 点位定时器信息 | |
| 64 | + */ | |
| 65 | + @Data | |
| 66 | + @AllArgsConstructor | |
| 67 | + private static class PointTimerInfo { | |
| 68 | + | |
| 69 | + /** | |
| 70 | + * Vert.x 定时器 ID | |
| 71 | + */ | |
| 72 | + private Long timerId; | |
| 73 | + /** | |
| 74 | + * 轮询间隔(用于判断是否需要更新定时器) | |
| 75 | + */ | |
| 76 | + private Integer pollInterval; | |
| 77 | + | |
| 78 | + } | |
| 79 | + | |
| 80 | + // ========== 轮询管理 ========== | |
| 81 | + | |
| 82 | + /** | |
| 83 | + * 更新轮询任务(增量更新) | |
| 84 | + * | |
| 85 | + * 1. 【删除】点位:停止对应的轮询定时器 | |
| 86 | + * 2. 【新增】点位:创建对应的轮询定时器 | |
| 87 | + * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器 | |
| 88 | + * 【修改】其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point) | |
| 89 | + */ | |
| 90 | + public void updatePolling(IotModbusDeviceConfigRespDTO config) { | |
| 91 | + Long deviceId = config.getDeviceId(); | |
| 92 | + List<IotModbusPointRespDTO> newPoints = config.getPoints(); | |
| 93 | + Map<Long, PointTimerInfo> currentTimers = devicePointTimers | |
| 94 | + .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>()); | |
| 95 | + // 1.1 计算新配置中的点位 ID 集合 | |
| 96 | + Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId); | |
| 97 | + // 1.2 计算删除的点位 ID 集合 | |
| 98 | + Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet()); | |
| 99 | + removedPointIds.removeAll(newPointIds); | |
| 100 | + | |
| 101 | + // 2. 处理删除的点位:停止不再存在的定时器 | |
| 102 | + for (Long pointId : removedPointIds) { | |
| 103 | + PointTimerInfo timerInfo = currentTimers.remove(pointId); | |
| 104 | + if (timerInfo != null) { | |
| 105 | + vertx.cancelTimer(timerInfo.getTimerId()); | |
| 106 | + log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId); | |
| 107 | + } | |
| 108 | + } | |
| 109 | + | |
| 110 | + // 3. 处理新增和修改的点位 | |
| 111 | + if (CollUtil.isEmpty(newPoints)) { | |
| 112 | + return; | |
| 113 | + } | |
| 114 | + for (IotModbusPointRespDTO point : newPoints) { | |
| 115 | + Long pointId = point.getId(); | |
| 116 | + Integer newPollInterval = point.getPollInterval(); | |
| 117 | + PointTimerInfo existingTimer = currentTimers.get(pointId); | |
| 118 | + // 3.1 新增点位:创建定时器 | |
| 119 | + if (existingTimer == null) { | |
| 120 | + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); | |
| 121 | + if (timerId != null) { | |
| 122 | + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); | |
| 123 | + log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", | |
| 124 | + deviceId, pointId, newPollInterval); | |
| 125 | + } | |
| 126 | + } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { | |
| 127 | + // 3.2 pollInterval 变化:重建定时器 | |
| 128 | + vertx.cancelTimer(existingTimer.getTimerId()); | |
| 129 | + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); | |
| 130 | + if (timerId != null) { | |
| 131 | + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); | |
| 132 | + log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", | |
| 133 | + deviceId, pointId, existingTimer.getPollInterval(), newPollInterval); | |
| 134 | + } else { | |
| 135 | + currentTimers.remove(pointId); | |
| 136 | + } | |
| 137 | + } | |
| 138 | + // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置 | |
| 139 | + } | |
| 140 | + } | |
| 141 | + | |
| 142 | + /** | |
| 143 | + * 创建轮询定时器 | |
| 144 | + */ | |
| 145 | + private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) { | |
| 146 | + if (pollInterval == null || pollInterval <= 0) { | |
| 147 | + return null; | |
| 148 | + } | |
| 149 | + return vertx.setPeriodic(pollInterval, timerId -> { | |
| 150 | + try { | |
| 151 | + submitPollRequest(deviceId, pointId); | |
| 152 | + } catch (Exception e) { | |
| 153 | + log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e); | |
| 154 | + } | |
| 155 | + }); | |
| 156 | + } | |
| 157 | + | |
| 158 | + // ========== 请求队列(per-device 限速) ========== | |
| 159 | + | |
| 160 | + /** | |
| 161 | + * 提交轮询请求到设备请求队列(保证同设备请求间隔) | |
| 162 | + */ | |
| 163 | + private void submitPollRequest(Long deviceId, Long pointId) { | |
| 164 | + // 1. 【重要】将请求添加到设备的请求队列 | |
| 165 | + Queue<Runnable> queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>()); | |
| 166 | + while (queue.size() >= MAX_QUEUE_SIZE) { | |
| 167 | + // 超出上限时,丢弃最旧的请求 | |
| 168 | + queue.poll(); | |
| 169 | + log.warn("[submitPollRequest][设备 {} 请求队列已满({}), 丢弃最旧请求]", deviceId, MAX_QUEUE_SIZE); | |
| 170 | + } | |
| 171 | + queue.offer(() -> pollPoint(deviceId, pointId)); | |
| 172 | + | |
| 173 | + // 2. 处理设备请求队列(如果没有延迟 timer 在等待) | |
| 174 | + processDeviceQueue(deviceId); | |
| 175 | + } | |
| 176 | + | |
| 177 | + /** | |
| 178 | + * 处理设备请求队列 | |
| 179 | + */ | |
| 180 | + private void processDeviceQueue(Long deviceId) { | |
| 181 | + Queue<Runnable> queue = deviceRequestQueues.get(deviceId); | |
| 182 | + if (CollUtil.isEmpty(queue)) { | |
| 183 | + return; | |
| 184 | + } | |
| 185 | + // 检查是否已有延迟 timer 在等待 | |
| 186 | + if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) { | |
| 187 | + return; | |
| 188 | + } | |
| 189 | + | |
| 190 | + // 不满足间隔要求,延迟执行 | |
| 191 | + long now = System.currentTimeMillis(); | |
| 192 | + long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L); | |
| 193 | + long elapsed = now - lastTime; | |
| 194 | + if (elapsed < MIN_REQUEST_INTERVAL) { | |
| 195 | + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed); | |
| 196 | + return; | |
| 197 | + } | |
| 198 | + | |
| 199 | + // 满足间隔要求,立即执行 | |
| 200 | + Runnable task = queue.poll(); | |
| 201 | + if (task == null) { | |
| 202 | + return; | |
| 203 | + } | |
| 204 | + deviceLastRequestTime.put(deviceId, now); | |
| 205 | + task.run(); | |
| 206 | + // 继续处理队列中的下一个(如果有的话,需要延迟) | |
| 207 | + if (CollUtil.isNotEmpty(queue)) { | |
| 208 | + scheduleNextRequest(deviceId); | |
| 209 | + } | |
| 210 | + } | |
| 211 | + | |
| 212 | + private void scheduleNextRequest(Long deviceId) { | |
| 213 | + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL); | |
| 214 | + } | |
| 215 | + | |
| 216 | + private void scheduleNextRequest(Long deviceId, long delayMs) { | |
| 217 | + deviceDelayTimerActive.put(deviceId, true); | |
| 218 | + vertx.setTimer(delayMs, id -> { | |
| 219 | + deviceDelayTimerActive.put(deviceId, false); | |
| 220 | + Queue<Runnable> queue = deviceRequestQueues.get(deviceId); | |
| 221 | + if (CollUtil.isEmpty(queue)) { | |
| 222 | + return; | |
| 223 | + } | |
| 224 | + | |
| 225 | + // 满足间隔要求,立即执行 | |
| 226 | + Runnable task = queue.poll(); | |
| 227 | + if (task == null) { | |
| 228 | + return; | |
| 229 | + } | |
| 230 | + deviceLastRequestTime.put(deviceId, System.currentTimeMillis()); | |
| 231 | + task.run(); | |
| 232 | + // 继续处理队列中的下一个(如果有的话,需要延迟) | |
| 233 | + if (CollUtil.isNotEmpty(queue)) { | |
| 234 | + scheduleNextRequest(deviceId); | |
| 235 | + } | |
| 236 | + }); | |
| 237 | + } | |
| 238 | + | |
| 239 | + // ========== 轮询执行 ========== | |
| 240 | + | |
| 241 | + /** | |
| 242 | + * 轮询单个点位(子类实现具体的读取逻辑) | |
| 243 | + * | |
| 244 | + * @param deviceId 设备 ID | |
| 245 | + * @param pointId 点位 ID | |
| 246 | + */ | |
| 247 | + protected abstract void pollPoint(Long deviceId, Long pointId); | |
| 248 | + | |
| 249 | + // ========== 停止 ========== | |
| 250 | + | |
| 251 | + /** | |
| 252 | + * 停止设备的轮询 | |
| 253 | + */ | |
| 254 | + public void stopPolling(Long deviceId) { | |
| 255 | + Map<Long, PointTimerInfo> timers = devicePointTimers.remove(deviceId); | |
| 256 | + if (CollUtil.isEmpty(timers)) { | |
| 257 | + return; | |
| 258 | + } | |
| 259 | + for (PointTimerInfo timerInfo : timers.values()) { | |
| 260 | + vertx.cancelTimer(timerInfo.getTimerId()); | |
| 261 | + } | |
| 262 | + // 清理请求队列 | |
| 263 | + deviceRequestQueues.remove(deviceId); | |
| 264 | + deviceLastRequestTime.remove(deviceId); | |
| 265 | + deviceDelayTimerActive.remove(deviceId); | |
| 266 | + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); | |
| 267 | + } | |
| 268 | + | |
| 269 | + /** | |
| 270 | + * 停止所有轮询 | |
| 271 | + */ | |
| 272 | + public void stopAll() { | |
| 273 | + for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) { | |
| 274 | + stopPolling(deviceId); | |
| 275 | + } | |
| 276 | + } | |
| 277 | + | |
| 278 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.util.ArrayUtil; | |
| 5 | +import cn.hutool.core.util.ObjectUtil; | |
| 6 | +import cn.hutool.core.util.StrUtil; | |
| 7 | +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | |
| 10 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum; | |
| 11 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; | |
| 13 | +import lombok.experimental.UtilityClass; | |
| 14 | +import lombok.extern.slf4j.Slf4j; | |
| 15 | + | |
| 16 | +import java.math.BigDecimal; | |
| 17 | +import java.math.RoundingMode; | |
| 18 | +import java.nio.ByteBuffer; | |
| 19 | +import java.nio.ByteOrder; | |
| 20 | + | |
| 21 | +/** | |
| 22 | + * IoT Modbus 协议工具类 | |
| 23 | + * <p> | |
| 24 | + * 提供 Modbus 协议全链路能力: | |
| 25 | + * <ul> | |
| 26 | + * <li>协议常量:功能码(FC01~FC16)、异常掩码等</li> | |
| 27 | + * <li>功能码判断:读/写/异常分类、可写判断、写功能码映射</li> | |
| 28 | + * <li>CRC-16/MODBUS 计算和校验</li> | |
| 29 | + * <li>数据转换:原始值 ↔ 物模型属性值({@link #convertToPropertyValue} / {@link #convertToRawValues})</li> | |
| 30 | + * <li>帧值提取:从 Modbus 帧提取寄存器/线圈值({@link #extractValues})</li> | |
| 31 | + * <li>点位查找({@link #findPoint})</li> | |
| 32 | + * </ul> | |
| 33 | + * | |
| 34 | + * @author 芋道源码 | |
| 35 | + */ | |
| 36 | +@UtilityClass | |
| 37 | +@Slf4j | |
| 38 | +public class IotModbusCommonUtils { | |
| 39 | + | |
| 40 | + /** FC01: 读线圈 */ | |
| 41 | + public static final int FC_READ_COILS = 1; | |
| 42 | + /** FC02: 读离散输入 */ | |
| 43 | + public static final int FC_READ_DISCRETE_INPUTS = 2; | |
| 44 | + /** FC03: 读保持寄存器 */ | |
| 45 | + public static final int FC_READ_HOLDING_REGISTERS = 3; | |
| 46 | + /** FC04: 读输入寄存器 */ | |
| 47 | + public static final int FC_READ_INPUT_REGISTERS = 4; | |
| 48 | + | |
| 49 | + /** FC05: 写单个线圈 */ | |
| 50 | + public static final int FC_WRITE_SINGLE_COIL = 5; | |
| 51 | + /** FC06: 写单个寄存器 */ | |
| 52 | + public static final int FC_WRITE_SINGLE_REGISTER = 6; | |
| 53 | + /** FC15: 写多个线圈 */ | |
| 54 | + public static final int FC_WRITE_MULTIPLE_COILS = 15; | |
| 55 | + /** FC16: 写多个寄存器 */ | |
| 56 | + public static final int FC_WRITE_MULTIPLE_REGISTERS = 16; | |
| 57 | + | |
| 58 | + /** | |
| 59 | + * 异常响应掩码:响应帧的功能码最高位为 1 时,表示异常响应 | |
| 60 | + * 例如:请求 FC=0x03,异常响应 FC=0x83(0x03 | 0x80) | |
| 61 | + */ | |
| 62 | + public static final int FC_EXCEPTION_MASK = 0x80; | |
| 63 | + | |
| 64 | + /** | |
| 65 | + * 功能码掩码:用于从异常响应中提取原始功能码 | |
| 66 | + * 例如:异常 FC=0x83,原始 FC = 0x83 & 0x7F = 0x03 | |
| 67 | + */ | |
| 68 | + public static final int FC_MASK = 0x7F; | |
| 69 | + | |
| 70 | + // ==================== 功能码分类判断 ==================== | |
| 71 | + | |
| 72 | + /** | |
| 73 | + * 判断是否为读响应(FC01-04) | |
| 74 | + */ | |
| 75 | + public static boolean isReadResponse(int functionCode) { | |
| 76 | + return functionCode >= FC_READ_COILS && functionCode <= FC_READ_INPUT_REGISTERS; | |
| 77 | + } | |
| 78 | + | |
| 79 | + /** | |
| 80 | + * 判断是否为写响应(FC05/06/15/16) | |
| 81 | + */ | |
| 82 | + public static boolean isWriteResponse(int functionCode) { | |
| 83 | + return functionCode == FC_WRITE_SINGLE_COIL || functionCode == FC_WRITE_SINGLE_REGISTER | |
| 84 | + || functionCode == FC_WRITE_MULTIPLE_COILS || functionCode == FC_WRITE_MULTIPLE_REGISTERS; | |
| 85 | + } | |
| 86 | + | |
| 87 | + /** | |
| 88 | + * 判断是否为异常响应 | |
| 89 | + */ | |
| 90 | + public static boolean isExceptionResponse(int functionCode) { | |
| 91 | + return (functionCode & FC_EXCEPTION_MASK) != 0; | |
| 92 | + } | |
| 93 | + | |
| 94 | + /** | |
| 95 | + * 从异常响应中提取原始功能码 | |
| 96 | + */ | |
| 97 | + public static int extractOriginalFunctionCode(int exceptionFunctionCode) { | |
| 98 | + return exceptionFunctionCode & FC_MASK; | |
| 99 | + } | |
| 100 | + | |
| 101 | + /** | |
| 102 | + * 判断读功能码是否支持写操作 | |
| 103 | + * <p> | |
| 104 | + * FC01(读线圈)和 FC03(读保持寄存器)支持写操作; | |
| 105 | + * FC02(读离散输入)和 FC04(读输入寄存器)为只读。 | |
| 106 | + * | |
| 107 | + * @param readFunctionCode 读功能码(FC01-04) | |
| 108 | + * @return 是否支持写操作 | |
| 109 | + */ | |
| 110 | + @SuppressWarnings("BooleanMethodIsAlwaysInverted") | |
| 111 | + public static boolean isWritable(int readFunctionCode) { | |
| 112 | + return readFunctionCode == FC_READ_COILS || readFunctionCode == FC_READ_HOLDING_REGISTERS; | |
| 113 | + } | |
| 114 | + | |
| 115 | + /** | |
| 116 | + * 获取单写功能码 | |
| 117 | + * <p> | |
| 118 | + * FC01(读线圈)→ FC05(写单个线圈); | |
| 119 | + * FC03(读保持寄存器)→ FC06(写单个寄存器); | |
| 120 | + * 其他返回 null(不支持写)。 | |
| 121 | + * | |
| 122 | + * @param readFunctionCode 读功能码 | |
| 123 | + * @return 单写功能码,不支持写时返回 null | |
| 124 | + */ | |
| 125 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 126 | + public static Integer getWriteSingleFunctionCode(int readFunctionCode) { | |
| 127 | + switch (readFunctionCode) { | |
| 128 | + case FC_READ_COILS: | |
| 129 | + return FC_WRITE_SINGLE_COIL; | |
| 130 | + case FC_READ_HOLDING_REGISTERS: | |
| 131 | + return FC_WRITE_SINGLE_REGISTER; | |
| 132 | + default: | |
| 133 | + return null; | |
| 134 | + } | |
| 135 | + } | |
| 136 | + | |
| 137 | + /** | |
| 138 | + * 获取多写功能码 | |
| 139 | + * <p> | |
| 140 | + * FC01(读线圈)→ FC15(写多个线圈); | |
| 141 | + * FC03(读保持寄存器)→ FC16(写多个寄存器); | |
| 142 | + * 其他返回 null(不支持写)。 | |
| 143 | + * | |
| 144 | + * @param readFunctionCode 读功能码 | |
| 145 | + * @return 多写功能码,不支持写时返回 null | |
| 146 | + */ | |
| 147 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 148 | + public static Integer getWriteMultipleFunctionCode(int readFunctionCode) { | |
| 149 | + switch (readFunctionCode) { | |
| 150 | + case FC_READ_COILS: | |
| 151 | + return FC_WRITE_MULTIPLE_COILS; | |
| 152 | + case FC_READ_HOLDING_REGISTERS: | |
| 153 | + return FC_WRITE_MULTIPLE_REGISTERS; | |
| 154 | + default: | |
| 155 | + return null; | |
| 156 | + } | |
| 157 | + } | |
| 158 | + | |
| 159 | + // ==================== CRC16 工具 ==================== | |
| 160 | + | |
| 161 | + /** | |
| 162 | + * 计算 CRC-16/MODBUS | |
| 163 | + * | |
| 164 | + * @param data 数据 | |
| 165 | + * @param length 计算长度 | |
| 166 | + * @return CRC16 值 | |
| 167 | + */ | |
| 168 | + public static int calculateCrc16(byte[] data, int length) { | |
| 169 | + int crc = 0xFFFF; | |
| 170 | + for (int i = 0; i < length; i++) { | |
| 171 | + crc ^= (data[i] & 0xFF); | |
| 172 | + for (int j = 0; j < 8; j++) { | |
| 173 | + if ((crc & 0x0001) != 0) { | |
| 174 | + crc >>= 1; | |
| 175 | + crc ^= 0xA001; | |
| 176 | + } else { | |
| 177 | + crc >>= 1; | |
| 178 | + } | |
| 179 | + } | |
| 180 | + } | |
| 181 | + return crc; | |
| 182 | + } | |
| 183 | + | |
| 184 | + /** | |
| 185 | + * 校验 CRC16 | |
| 186 | + * | |
| 187 | + * @param data 包含 CRC 的完整数据 | |
| 188 | + * @return 校验是否通过 | |
| 189 | + */ | |
| 190 | + public static boolean verifyCrc16(byte[] data) { | |
| 191 | + if (data.length < 3) { | |
| 192 | + return false; | |
| 193 | + } | |
| 194 | + int computed = calculateCrc16(data, data.length - 2); | |
| 195 | + int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8); | |
| 196 | + return computed == received; | |
| 197 | + } | |
| 198 | + | |
| 199 | + // ==================== 数据转换 ==================== | |
| 200 | + | |
| 201 | + /** | |
| 202 | + * 将原始值转换为物模型属性值 | |
| 203 | + * | |
| 204 | + * @param rawValues 原始值数组(寄存器值或线圈值) | |
| 205 | + * @param point 点位配置 | |
| 206 | + * @return 转换后的属性值 | |
| 207 | + */ | |
| 208 | + public static Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) { | |
| 209 | + if (ArrayUtil.isEmpty(rawValues)) { | |
| 210 | + return null; | |
| 211 | + } | |
| 212 | + String rawDataType = point.getRawDataType(); | |
| 213 | + String byteOrder = point.getByteOrder(); | |
| 214 | + BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); | |
| 215 | + | |
| 216 | + // 1. 根据原始数据类型解析原始数值 | |
| 217 | + Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder); | |
| 218 | + if (rawNumber == null) { | |
| 219 | + return null; | |
| 220 | + } | |
| 221 | + | |
| 222 | + // 2. 应用缩放因子:实际值 = 原始值 × scale | |
| 223 | + BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale); | |
| 224 | + | |
| 225 | + // 3. 根据数据类型返回合适的 Java 类型 | |
| 226 | + return formatValue(actualValue, rawDataType); | |
| 227 | + } | |
| 228 | + | |
| 229 | + /** | |
| 230 | + * 将物模型属性值转换为原始寄存器值 | |
| 231 | + * | |
| 232 | + * @param propertyValue 属性值 | |
| 233 | + * @param point 点位配置 | |
| 234 | + * @return 原始值数组 | |
| 235 | + */ | |
| 236 | + public static int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) { | |
| 237 | + if (propertyValue == null) { | |
| 238 | + return new int[0]; | |
| 239 | + } | |
| 240 | + String rawDataType = point.getRawDataType(); | |
| 241 | + String byteOrder = point.getByteOrder(); | |
| 242 | + BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); | |
| 243 | + int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1); | |
| 244 | + | |
| 245 | + // 1. 转换为 BigDecimal | |
| 246 | + BigDecimal actualValue = new BigDecimal(propertyValue.toString()); | |
| 247 | + | |
| 248 | + // 2. 应用缩放因子:原始值 = 实际值 ÷ scale | |
| 249 | + BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP); | |
| 250 | + | |
| 251 | + // 3. 根据原始数据类型编码为寄存器值 | |
| 252 | + return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount); | |
| 253 | + } | |
| 254 | + | |
| 255 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 256 | + private static Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) { | |
| 257 | + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); | |
| 258 | + if (dataTypeEnum == null) { | |
| 259 | + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); | |
| 260 | + return rawValues[0]; | |
| 261 | + } | |
| 262 | + switch (dataTypeEnum) { | |
| 263 | + case BOOLEAN: | |
| 264 | + return rawValues[0] != 0 ? 1 : 0; | |
| 265 | + case INT16: | |
| 266 | + return (short) rawValues[0]; | |
| 267 | + case UINT16: | |
| 268 | + return rawValues[0] & 0xFFFF; | |
| 269 | + case INT32: | |
| 270 | + return parseInt32(rawValues, byteOrder); | |
| 271 | + case UINT32: | |
| 272 | + return parseUint32(rawValues, byteOrder); | |
| 273 | + case FLOAT: | |
| 274 | + return parseFloat(rawValues, byteOrder); | |
| 275 | + case DOUBLE: | |
| 276 | + return parseDouble(rawValues, byteOrder); | |
| 277 | + default: | |
| 278 | + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); | |
| 279 | + return rawValues[0]; | |
| 280 | + } | |
| 281 | + } | |
| 282 | + | |
| 283 | + private static int parseInt32(int[] rawValues, String byteOrder) { | |
| 284 | + if (rawValues.length < 2) { | |
| 285 | + return rawValues[0]; | |
| 286 | + } | |
| 287 | + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); | |
| 288 | + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt(); | |
| 289 | + } | |
| 290 | + | |
| 291 | + private static long parseUint32(int[] rawValues, String byteOrder) { | |
| 292 | + if (rawValues.length < 2) { | |
| 293 | + return rawValues[0] & 0xFFFFFFFFL; | |
| 294 | + } | |
| 295 | + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); | |
| 296 | + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL; | |
| 297 | + } | |
| 298 | + | |
| 299 | + private static float parseFloat(int[] rawValues, String byteOrder) { | |
| 300 | + if (rawValues.length < 2) { | |
| 301 | + return (float) rawValues[0]; | |
| 302 | + } | |
| 303 | + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); | |
| 304 | + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat(); | |
| 305 | + } | |
| 306 | + | |
| 307 | + private static double parseDouble(int[] rawValues, String byteOrder) { | |
| 308 | + if (rawValues.length < 4) { | |
| 309 | + return rawValues[0]; | |
| 310 | + } | |
| 311 | + byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder); | |
| 312 | + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble(); | |
| 313 | + } | |
| 314 | + | |
| 315 | + private static byte[] registersToBytes(int[] registers, int count) { | |
| 316 | + byte[] bytes = new byte[count * 2]; | |
| 317 | + for (int i = 0; i < Math.min(registers.length, count); i++) { | |
| 318 | + bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF); | |
| 319 | + bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF); | |
| 320 | + } | |
| 321 | + return bytes; | |
| 322 | + } | |
| 323 | + | |
| 324 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 325 | + private static byte[] reorderBytes(byte[] bytes, String byteOrder) { | |
| 326 | + IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder); | |
| 327 | + // null 或者大端序,不需要调整 | |
| 328 | + if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) { | |
| 329 | + return bytes; | |
| 330 | + } | |
| 331 | + | |
| 332 | + // 其他字节序调整 | |
| 333 | + byte[] result = new byte[bytes.length]; | |
| 334 | + switch (byteOrderEnum) { | |
| 335 | + case BA: // 小端序:按每 2 字节一组交换(16 位场景 [1,0],32 位场景 [1,0,3,2]) | |
| 336 | + for (int i = 0; i + 1 < bytes.length; i += 2) { | |
| 337 | + result[i] = bytes[i + 1]; | |
| 338 | + result[i + 1] = bytes[i]; | |
| 339 | + } | |
| 340 | + break; | |
| 341 | + case CDAB: // 大端字交换(32 位) | |
| 342 | + if (bytes.length >= 4) { | |
| 343 | + result[0] = bytes[2]; | |
| 344 | + result[1] = bytes[3]; | |
| 345 | + result[2] = bytes[0]; | |
| 346 | + result[3] = bytes[1]; | |
| 347 | + } | |
| 348 | + break; | |
| 349 | + case DCBA: // 小端序(32 位) | |
| 350 | + if (bytes.length >= 4) { | |
| 351 | + result[0] = bytes[3]; | |
| 352 | + result[1] = bytes[2]; | |
| 353 | + result[2] = bytes[1]; | |
| 354 | + result[3] = bytes[0]; | |
| 355 | + } | |
| 356 | + break; | |
| 357 | + case BADC: // 小端字交换(32 位) | |
| 358 | + if (bytes.length >= 4) { | |
| 359 | + result[0] = bytes[1]; | |
| 360 | + result[1] = bytes[0]; | |
| 361 | + result[2] = bytes[3]; | |
| 362 | + result[3] = bytes[2]; | |
| 363 | + } | |
| 364 | + break; | |
| 365 | + default: | |
| 366 | + return bytes; | |
| 367 | + } | |
| 368 | + return result; | |
| 369 | + } | |
| 370 | + | |
| 371 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 372 | + private static int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) { | |
| 373 | + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); | |
| 374 | + if (dataTypeEnum == null) { | |
| 375 | + return new int[]{rawValue.intValue()}; | |
| 376 | + } | |
| 377 | + switch (dataTypeEnum) { | |
| 378 | + case BOOLEAN: | |
| 379 | + return new int[]{rawValue.intValue() != 0 ? 1 : 0}; | |
| 380 | + case INT16: | |
| 381 | + case UINT16: | |
| 382 | + return new int[]{rawValue.intValue() & 0xFFFF}; | |
| 383 | + case INT32: | |
| 384 | + return encodeInt32(rawValue.intValue(), byteOrder); | |
| 385 | + case UINT32: | |
| 386 | + // 使用 longValue() 避免超过 Integer.MAX_VALUE 时溢出, | |
| 387 | + // 强转 int 保留低 32 位 bit pattern,写入寄存器的字节是正确的无符号值 | |
| 388 | + return encodeInt32((int) rawValue.longValue(), byteOrder); | |
| 389 | + case FLOAT: | |
| 390 | + return encodeFloat(rawValue.floatValue(), byteOrder); | |
| 391 | + case DOUBLE: | |
| 392 | + return encodeDouble(rawValue.doubleValue(), byteOrder); | |
| 393 | + default: | |
| 394 | + return new int[]{rawValue.intValue()}; | |
| 395 | + } | |
| 396 | + } | |
| 397 | + | |
| 398 | + private static int[] encodeInt32(int value, String byteOrder) { | |
| 399 | + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array(); | |
| 400 | + bytes = reorderBytes(bytes, byteOrder); | |
| 401 | + return bytesToRegisters(bytes); | |
| 402 | + } | |
| 403 | + | |
| 404 | + private static int[] encodeFloat(float value, String byteOrder) { | |
| 405 | + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array(); | |
| 406 | + bytes = reorderBytes(bytes, byteOrder); | |
| 407 | + return bytesToRegisters(bytes); | |
| 408 | + } | |
| 409 | + | |
| 410 | + private static int[] encodeDouble(double value, String byteOrder) { | |
| 411 | + byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array(); | |
| 412 | + bytes = reorderBytes(bytes, byteOrder); | |
| 413 | + return bytesToRegisters(bytes); | |
| 414 | + } | |
| 415 | + | |
| 416 | + private static int[] bytesToRegisters(byte[] bytes) { | |
| 417 | + int[] registers = new int[bytes.length / 2]; | |
| 418 | + for (int i = 0; i < registers.length; i++) { | |
| 419 | + registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF); | |
| 420 | + } | |
| 421 | + return registers; | |
| 422 | + } | |
| 423 | + | |
| 424 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 425 | + private static Object formatValue(BigDecimal value, String rawDataType) { | |
| 426 | + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); | |
| 427 | + if (dataTypeEnum == null) { | |
| 428 | + return value; | |
| 429 | + } | |
| 430 | + switch (dataTypeEnum) { | |
| 431 | + case BOOLEAN: | |
| 432 | + return value.intValue() != 0; | |
| 433 | + case INT16: | |
| 434 | + case INT32: | |
| 435 | + return value.intValue(); | |
| 436 | + case UINT16: | |
| 437 | + case UINT32: | |
| 438 | + return value.longValue(); | |
| 439 | + case FLOAT: | |
| 440 | + return value.floatValue(); | |
| 441 | + case DOUBLE: | |
| 442 | + return value.doubleValue(); | |
| 443 | + default: | |
| 444 | + return value; | |
| 445 | + } | |
| 446 | + } | |
| 447 | + | |
| 448 | + // ==================== 帧值提取 ==================== | |
| 449 | + | |
| 450 | + /** | |
| 451 | + * 从帧中提取寄存器值(FC01-04 读响应) | |
| 452 | + * | |
| 453 | + * @param frame 解码后的 Modbus 帧 | |
| 454 | + * @return 寄存器值数组(int[]),失败返回 null | |
| 455 | + */ | |
| 456 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 457 | + public static int[] extractValues(IotModbusFrame frame) { | |
| 458 | + if (frame == null || frame.isException()) { | |
| 459 | + return null; | |
| 460 | + } | |
| 461 | + byte[] pdu = frame.getPdu(); | |
| 462 | + if (pdu == null || pdu.length < 1) { | |
| 463 | + return null; | |
| 464 | + } | |
| 465 | + | |
| 466 | + int functionCode = frame.getFunctionCode(); | |
| 467 | + switch (functionCode) { | |
| 468 | + case FC_READ_COILS: | |
| 469 | + case FC_READ_DISCRETE_INPUTS: | |
| 470 | + return extractCoilValues(pdu); | |
| 471 | + case FC_READ_HOLDING_REGISTERS: | |
| 472 | + case FC_READ_INPUT_REGISTERS: | |
| 473 | + return extractRegisterValues(pdu); | |
| 474 | + default: | |
| 475 | + log.warn("[extractValues][不支持的功能码: {}]", functionCode); | |
| 476 | + return null; | |
| 477 | + } | |
| 478 | + } | |
| 479 | + | |
| 480 | + private static int[] extractCoilValues(byte[] pdu) { | |
| 481 | + if (pdu.length < 2) { | |
| 482 | + return null; | |
| 483 | + } | |
| 484 | + int byteCount = pdu[0] & 0xFF; | |
| 485 | + int bitCount = byteCount * 8; | |
| 486 | + int[] values = new int[bitCount]; | |
| 487 | + for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) { | |
| 488 | + values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01); | |
| 489 | + } | |
| 490 | + return values; | |
| 491 | + } | |
| 492 | + | |
| 493 | + private static int[] extractRegisterValues(byte[] pdu) { | |
| 494 | + if (pdu.length < 2) { | |
| 495 | + return null; | |
| 496 | + } | |
| 497 | + int byteCount = pdu[0] & 0xFF; | |
| 498 | + int registerCount = byteCount / 2; | |
| 499 | + int[] values = new int[registerCount]; | |
| 500 | + for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) { | |
| 501 | + values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF); | |
| 502 | + } | |
| 503 | + return values; | |
| 504 | + } | |
| 505 | + | |
| 506 | + /** | |
| 507 | + * 从响应帧中提取 registerCount(通过 PDU 的 byteCount 推断) | |
| 508 | + * | |
| 509 | + * @param frame 解码后的 Modbus 响应帧 | |
| 510 | + * @return registerCount,无法提取时返回 -1(匹配时跳过校验) | |
| 511 | + */ | |
| 512 | + public static int extractRegisterCountFromResponse(IotModbusFrame frame) { | |
| 513 | + byte[] pdu = frame.getPdu(); | |
| 514 | + if (pdu == null || pdu.length < 1) { | |
| 515 | + return -1; | |
| 516 | + } | |
| 517 | + int byteCount = pdu[0] & 0xFF; | |
| 518 | + int fc = frame.getFunctionCode(); | |
| 519 | + // FC03/04 寄存器读响应:registerCount = byteCount / 2 | |
| 520 | + if (fc == FC_READ_HOLDING_REGISTERS || fc == FC_READ_INPUT_REGISTERS) { | |
| 521 | + return byteCount / 2; | |
| 522 | + } | |
| 523 | + // FC01/02 线圈/离散输入读响应:按 bit 打包有余位,无法精确反推,返回 -1 跳过校验 | |
| 524 | + return -1; | |
| 525 | + } | |
| 526 | + | |
| 527 | + // ==================== 点位查找 ==================== | |
| 528 | + | |
| 529 | + /** | |
| 530 | + * 查找点位配置 | |
| 531 | + * | |
| 532 | + * @param config 设备 Modbus 配置 | |
| 533 | + * @param identifier 点位标识符 | |
| 534 | + * @return 匹配的点位配置,未找到返回 null | |
| 535 | + */ | |
| 536 | + public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { | |
| 537 | + if (config == null || StrUtil.isBlank(identifier)) { | |
| 538 | + return null; | |
| 539 | + } | |
| 540 | + return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); | |
| 541 | + } | |
| 542 | + | |
| 543 | + /** | |
| 544 | + * 根据点位 ID 查找点位配置 | |
| 545 | + * | |
| 546 | + * @param config 设备 Modbus 配置 | |
| 547 | + * @param pointId 点位 ID | |
| 548 | + * @return 匹配的点位配置,未找到返回 null | |
| 549 | + */ | |
| 550 | + public static IotModbusPointRespDTO findPointById(IotModbusDeviceConfigRespDTO config, Long pointId) { | |
| 551 | + if (config == null || pointId == null) { | |
| 552 | + return null; | |
| 553 | + } | |
| 554 | + return CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId)); | |
| 555 | + } | |
| 556 | + | |
| 557 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager; | |
| 5 | +import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; | |
| 6 | +import com.ghgande.j2mod.modbus.msg.*; | |
| 7 | +import com.ghgande.j2mod.modbus.procimg.InputRegister; | |
| 8 | +import com.ghgande.j2mod.modbus.procimg.Register; | |
| 9 | +import com.ghgande.j2mod.modbus.procimg.SimpleRegister; | |
| 10 | +import com.ghgande.j2mod.modbus.util.BitVector; | |
| 11 | +import io.vertx.core.Future; | |
| 12 | +import lombok.experimental.UtilityClass; | |
| 13 | +import lombok.extern.slf4j.Slf4j; | |
| 14 | + | |
| 15 | +import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.*; | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * IoT Modbus TCP 客户端工具类 | |
| 19 | + * <p> | |
| 20 | + * 封装基于 j2mod 的 Modbus TCP 读写操作: | |
| 21 | + * 1. 根据功能码创建对应的 Modbus 读/写请求 | |
| 22 | + * 2. 通过 {@link IotModbusTcpClientConnectionManager.ModbusConnection} 执行事务 | |
| 23 | + * 3. 从响应中提取原始值 | |
| 24 | + * | |
| 25 | + * @author 芋道源码 | |
| 26 | + */ | |
| 27 | +@UtilityClass | |
| 28 | +@Slf4j | |
| 29 | +public class IotModbusTcpClientUtils { | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 读取 Modbus 数据 | |
| 33 | + * | |
| 34 | + * @param connection Modbus 连接 | |
| 35 | + * @param slaveId 从站地址 | |
| 36 | + * @param point 点位配置 | |
| 37 | + * @return 原始值(int 数组) | |
| 38 | + */ | |
| 39 | + public static Future<int[]> read(IotModbusTcpClientConnectionManager.ModbusConnection connection, | |
| 40 | + Integer slaveId, | |
| 41 | + IotModbusPointRespDTO point) { | |
| 42 | + return connection.executeBlocking(tcpConnection -> { | |
| 43 | + try { | |
| 44 | + // 1. 创建请求 | |
| 45 | + ModbusRequest request = createReadRequest(point.getFunctionCode(), | |
| 46 | + point.getRegisterAddress(), point.getRegisterCount()); | |
| 47 | + request.setUnitID(slaveId); | |
| 48 | + | |
| 49 | + // 2. 执行事务(请求) | |
| 50 | + ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection); | |
| 51 | + transaction.setRequest(request); | |
| 52 | + transaction.execute(); | |
| 53 | + | |
| 54 | + // 3. 解析响应 | |
| 55 | + ModbusResponse response = transaction.getResponse(); | |
| 56 | + return extractValues(response, point.getFunctionCode()); | |
| 57 | + } catch (Exception e) { | |
| 58 | + throw new RuntimeException(String.format("Modbus 读取失败 [slaveId=%d, identifier=%s, address=%d]", | |
| 59 | + slaveId, point.getIdentifier(), point.getRegisterAddress()), e); | |
| 60 | + } | |
| 61 | + }); | |
| 62 | + } | |
| 63 | + | |
| 64 | + /** | |
| 65 | + * 写入 Modbus 数据 | |
| 66 | + * | |
| 67 | + * @param connection Modbus 连接 | |
| 68 | + * @param slaveId 从站地址 | |
| 69 | + * @param point 点位配置 | |
| 70 | + * @param values 要写入的值 | |
| 71 | + * @return 是否成功 | |
| 72 | + */ | |
| 73 | + public static Future<Boolean> write(IotModbusTcpClientConnectionManager.ModbusConnection connection, | |
| 74 | + Integer slaveId, | |
| 75 | + IotModbusPointRespDTO point, | |
| 76 | + int[] values) { | |
| 77 | + return connection.executeBlocking(tcpConnection -> { | |
| 78 | + try { | |
| 79 | + // 1. 创建请求 | |
| 80 | + ModbusRequest request = createWriteRequest(point.getFunctionCode(), | |
| 81 | + point.getRegisterAddress(), point.getRegisterCount(), values); | |
| 82 | + if (request == null) { | |
| 83 | + throw new RuntimeException("功能码 " + point.getFunctionCode() + " 不支持写操作"); | |
| 84 | + } | |
| 85 | + request.setUnitID(slaveId); | |
| 86 | + | |
| 87 | + // 2. 执行事务(请求) | |
| 88 | + ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection); | |
| 89 | + transaction.setRequest(request); | |
| 90 | + transaction.execute(); | |
| 91 | + return true; | |
| 92 | + } catch (Exception e) { | |
| 93 | + throw new RuntimeException(String.format("Modbus 写入失败 [slaveId=%d, identifier=%s, address=%d]", | |
| 94 | + slaveId, point.getIdentifier(), point.getRegisterAddress()), e); | |
| 95 | + } | |
| 96 | + }); | |
| 97 | + } | |
| 98 | + | |
| 99 | + /** | |
| 100 | + * 创建读取请求 | |
| 101 | + */ | |
| 102 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 103 | + private static ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { | |
| 104 | + switch (functionCode) { | |
| 105 | + case FC_READ_COILS: | |
| 106 | + return new ReadCoilsRequest(address, count); | |
| 107 | + case FC_READ_DISCRETE_INPUTS: | |
| 108 | + return new ReadInputDiscretesRequest(address, count); | |
| 109 | + case FC_READ_HOLDING_REGISTERS: | |
| 110 | + return new ReadMultipleRegistersRequest(address, count); | |
| 111 | + case FC_READ_INPUT_REGISTERS: | |
| 112 | + return new ReadInputRegistersRequest(address, count); | |
| 113 | + default: | |
| 114 | + throw new IllegalArgumentException("不支持的功能码: " + functionCode); | |
| 115 | + } | |
| 116 | + } | |
| 117 | + | |
| 118 | + /** | |
| 119 | + * 创建写入请求 | |
| 120 | + */ | |
| 121 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 122 | + private static ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { | |
| 123 | + switch (functionCode) { | |
| 124 | + case FC_READ_COILS: // 写线圈(使用功能码 5 或 15) | |
| 125 | + if (count == 1) { | |
| 126 | + return new WriteCoilRequest(address, values[0] != 0); | |
| 127 | + } else { | |
| 128 | + BitVector bv = new BitVector(count); | |
| 129 | + for (int i = 0; i < Math.min(values.length, count); i++) { | |
| 130 | + bv.setBit(i, values[i] != 0); | |
| 131 | + } | |
| 132 | + return new WriteMultipleCoilsRequest(address, bv); | |
| 133 | + } | |
| 134 | + case FC_READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16) | |
| 135 | + if (count == 1) { | |
| 136 | + return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0])); | |
| 137 | + } else { | |
| 138 | + Register[] registers = new SimpleRegister[count]; | |
| 139 | + for (int i = 0; i < count; i++) { | |
| 140 | + registers[i] = new SimpleRegister(i < values.length ? values[i] : 0); | |
| 141 | + } | |
| 142 | + return new WriteMultipleRegistersRequest(address, registers); | |
| 143 | + } | |
| 144 | + case FC_READ_DISCRETE_INPUTS: // 只读 | |
| 145 | + case FC_READ_INPUT_REGISTERS: // 只读 | |
| 146 | + return null; | |
| 147 | + default: | |
| 148 | + throw new IllegalArgumentException("不支持的功能码: " + functionCode); | |
| 149 | + } | |
| 150 | + } | |
| 151 | + | |
| 152 | + /** | |
| 153 | + * 从响应中提取值 | |
| 154 | + */ | |
| 155 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 156 | + private static int[] extractValues(ModbusResponse response, Integer functionCode) { | |
| 157 | + switch (functionCode) { | |
| 158 | + case FC_READ_COILS: | |
| 159 | + ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; | |
| 160 | + int bitCount = coilsResponse.getBitCount(); | |
| 161 | + int[] coilValues = new int[bitCount]; | |
| 162 | + for (int i = 0; i < bitCount; i++) { | |
| 163 | + coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0; | |
| 164 | + } | |
| 165 | + return coilValues; | |
| 166 | + case FC_READ_DISCRETE_INPUTS: | |
| 167 | + ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response; | |
| 168 | + int discreteCount = discretesResponse.getBitCount(); | |
| 169 | + int[] discreteValues = new int[discreteCount]; | |
| 170 | + for (int i = 0; i < discreteCount; i++) { | |
| 171 | + discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0; | |
| 172 | + } | |
| 173 | + return discreteValues; | |
| 174 | + case FC_READ_HOLDING_REGISTERS: | |
| 175 | + ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response; | |
| 176 | + InputRegister[] holdingRegisters = holdingResponse.getRegisters(); | |
| 177 | + int[] holdingValues = new int[holdingRegisters.length]; | |
| 178 | + for (int i = 0; i < holdingRegisters.length; i++) { | |
| 179 | + holdingValues[i] = holdingRegisters[i].getValue(); | |
| 180 | + } | |
| 181 | + return holdingValues; | |
| 182 | + case FC_READ_INPUT_REGISTERS: | |
| 183 | + ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response; | |
| 184 | + InputRegister[] inputRegisters = inputResponse.getRegisters(); | |
| 185 | + int[] inputValues = new int[inputRegisters.length]; | |
| 186 | + for (int i = 0; i < inputRegisters.length; i++) { | |
| 187 | + inputValues[i] = inputRegisters[i].getValue(); | |
| 188 | + } | |
| 189 | + return inputValues; | |
| 190 | + default: | |
| 191 | + throw new IllegalArgumentException("不支持的功能码: " + functionCode); | |
| 192 | + } | |
| 193 | + } | |
| 194 | + | |
| 195 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientConfig.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient; | |
| 2 | + | |
| 3 | +import jakarta.validation.constraints.Min; | |
| 4 | +import jakarta.validation.constraints.NotNull; | |
| 5 | +import lombok.Data; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * IoT Modbus TCP Client 协议配置 | |
| 9 | + * | |
| 10 | + * @author 芋道源码 | |
| 11 | + */ | |
| 12 | +@Data | |
| 13 | +public class IotModbusTcpClientConfig { | |
| 14 | + | |
| 15 | + /** | |
| 16 | + * 配置刷新间隔(秒) | |
| 17 | + */ | |
| 18 | + @NotNull(message = "配置刷新间隔不能为空") | |
| 19 | + @Min(value = 1, message = "配置刷新间隔不能小于 1 秒") | |
| 20 | + private Integer configRefreshInterval = 30; | |
| 21 | + | |
| 22 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.extra.spring.SpringUtil; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamHandler; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamSubscriber; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler; | |
| 15 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService; | |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager; | |
| 17 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientPollScheduler; | |
| 18 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 19 | +import io.vertx.core.Vertx; | |
| 20 | +import lombok.Getter; | |
| 21 | +import lombok.extern.slf4j.Slf4j; | |
| 22 | +import org.redisson.api.RedissonClient; | |
| 23 | + | |
| 24 | +import java.util.List; | |
| 25 | +import java.util.Set; | |
| 26 | +import java.util.concurrent.TimeUnit; | |
| 27 | + | |
| 28 | +/** | |
| 29 | + * IoT 网关 Modbus TCP Client 协议:主动轮询 Modbus 从站设备数据 | |
| 30 | + * | |
| 31 | + * @author 芋道源码 | |
| 32 | + */ | |
| 33 | +@Slf4j | |
| 34 | +public class IotModbusTcpClientProtocol implements IotProtocol { | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * 协议配置 | |
| 38 | + */ | |
| 39 | + private final ProtocolProperties properties; | |
| 40 | + /** | |
| 41 | + * 服务器 ID(用于消息追踪,全局唯一) | |
| 42 | + */ | |
| 43 | + @Getter | |
| 44 | + private final String serverId; | |
| 45 | + | |
| 46 | + /** | |
| 47 | + * 运行状态 | |
| 48 | + */ | |
| 49 | + @Getter | |
| 50 | + private volatile boolean running = false; | |
| 51 | + | |
| 52 | + /** | |
| 53 | + * Vert.x 实例 | |
| 54 | + */ | |
| 55 | + private final Vertx vertx; | |
| 56 | + /** | |
| 57 | + * 配置刷新定时器 ID | |
| 58 | + */ | |
| 59 | + private Long configRefreshTimerId; | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * 连接管理器 | |
| 63 | + */ | |
| 64 | + private final IotModbusTcpClientConnectionManager connectionManager; | |
| 65 | + /** | |
| 66 | + * 下行消息订阅者 | |
| 67 | + */ | |
| 68 | + private IotModbusTcpClientDownstreamSubscriber downstreamSubscriber; | |
| 69 | + | |
| 70 | + private final IotModbusTcpClientConfigCacheService configCacheService; | |
| 71 | + private final IotModbusTcpClientPollScheduler pollScheduler; | |
| 72 | + | |
| 73 | + public IotModbusTcpClientProtocol(ProtocolProperties properties) { | |
| 74 | + IotModbusTcpClientConfig modbusTcpClientConfig = properties.getModbusTcpClient(); | |
| 75 | + Assert.notNull(modbusTcpClientConfig, "Modbus TCP Client 协议配置(modbusTcpClient)不能为空"); | |
| 76 | + this.properties = properties; | |
| 77 | + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | |
| 78 | + | |
| 79 | + // 初始化 Vertx | |
| 80 | + this.vertx = Vertx.vertx(); | |
| 81 | + | |
| 82 | + // 初始化 Manager | |
| 83 | + RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class); | |
| 84 | + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 85 | + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 86 | + this.configCacheService = new IotModbusTcpClientConfigCacheService(deviceApi); | |
| 87 | + this.connectionManager = new IotModbusTcpClientConnectionManager(redissonClient, vertx, | |
| 88 | + messageService, configCacheService, serverId); | |
| 89 | + | |
| 90 | + // 初始化 Handler | |
| 91 | + IotModbusTcpClientUpstreamHandler upstreamHandler = new IotModbusTcpClientUpstreamHandler(messageService, serverId); | |
| 92 | + | |
| 93 | + // 初始化轮询调度器 | |
| 94 | + this.pollScheduler = new IotModbusTcpClientPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService); | |
| 95 | + } | |
| 96 | + | |
| 97 | + @Override | |
| 98 | + public String getId() { | |
| 99 | + return properties.getId(); | |
| 100 | + } | |
| 101 | + | |
| 102 | + @Override | |
| 103 | + public IotProtocolTypeEnum getType() { | |
| 104 | + return IotProtocolTypeEnum.MODBUS_TCP_CLIENT; | |
| 105 | + } | |
| 106 | + | |
| 107 | + @Override | |
| 108 | + public void start() { | |
| 109 | + if (running) { | |
| 110 | + log.warn("[start][IoT Modbus TCP Client 协议 {} 已经在运行中]", getId()); | |
| 111 | + return; | |
| 112 | + } | |
| 113 | + | |
| 114 | + try { | |
| 115 | + // 1.1 首次加载配置 | |
| 116 | + refreshConfig(); | |
| 117 | + // 1.2 启动配置刷新定时器 | |
| 118 | + int refreshInterval = properties.getModbusTcpClient().getConfigRefreshInterval(); | |
| 119 | + configRefreshTimerId = vertx.setPeriodic( | |
| 120 | + TimeUnit.SECONDS.toMillis(refreshInterval), | |
| 121 | + id -> refreshConfig() | |
| 122 | + ); | |
| 123 | + running = true; | |
| 124 | + log.info("[start][IoT Modbus TCP Client 协议 {} 启动成功,serverId={}]", getId(), serverId); | |
| 125 | + | |
| 126 | + // 2. 启动下行消息订阅者 | |
| 127 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 128 | + IotModbusTcpClientDownstreamHandler downstreamHandler = new IotModbusTcpClientDownstreamHandler(connectionManager, | |
| 129 | + configCacheService); | |
| 130 | + this.downstreamSubscriber = new IotModbusTcpClientDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 131 | + this.downstreamSubscriber.start(); | |
| 132 | + } catch (Exception e) { | |
| 133 | + log.error("[start][IoT Modbus TCP Client 协议 {} 启动失败]", getId(), e); | |
| 134 | + stop0(); | |
| 135 | + throw e; | |
| 136 | + } | |
| 137 | + } | |
| 138 | + | |
| 139 | + @Override | |
| 140 | + public void stop() { | |
| 141 | + if (!running) { | |
| 142 | + return; | |
| 143 | + } | |
| 144 | + stop0(); | |
| 145 | + } | |
| 146 | + | |
| 147 | + private void stop0() { | |
| 148 | + // 1. 停止下行消息订阅者 | |
| 149 | + if (downstreamSubscriber != null) { | |
| 150 | + try { | |
| 151 | + downstreamSubscriber.stop(); | |
| 152 | + log.info("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者已停止]", getId()); | |
| 153 | + } catch (Exception e) { | |
| 154 | + log.error("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 155 | + } | |
| 156 | + downstreamSubscriber = null; | |
| 157 | + } | |
| 158 | + | |
| 159 | + // 2.1 取消配置刷新定时器 | |
| 160 | + if (configRefreshTimerId != null) { | |
| 161 | + vertx.cancelTimer(configRefreshTimerId); | |
| 162 | + configRefreshTimerId = null; | |
| 163 | + } | |
| 164 | + // 2.2 停止轮询调度器 | |
| 165 | + pollScheduler.stopAll(); | |
| 166 | + // 2.3 关闭所有连接 | |
| 167 | + connectionManager.closeAll(); | |
| 168 | + | |
| 169 | + // 3. 关闭 Vert.x 实例 | |
| 170 | + if (vertx != null) { | |
| 171 | + try { | |
| 172 | + vertx.close().result(); | |
| 173 | + log.info("[stop][IoT Modbus TCP Client 协议 {} Vertx 已关闭]", getId()); | |
| 174 | + } catch (Exception e) { | |
| 175 | + log.error("[stop][IoT Modbus TCP Client 协议 {} Vertx 关闭失败]", getId(), e); | |
| 176 | + } | |
| 177 | + } | |
| 178 | + running = false; | |
| 179 | + log.info("[stop][IoT Modbus TCP Client 协议 {} 已停止]", getId()); | |
| 180 | + } | |
| 181 | + | |
| 182 | + /** | |
| 183 | + * 刷新配置 | |
| 184 | + */ | |
| 185 | + private synchronized void refreshConfig() { | |
| 186 | + try { | |
| 187 | + // 1. 从 biz 拉取最新配置(API 失败时返回 null) | |
| 188 | + List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig(); | |
| 189 | + if (configs == null) { | |
| 190 | + log.warn("[refreshConfig][API 失败,跳过本轮刷新]"); | |
| 191 | + return; | |
| 192 | + } | |
| 193 | + log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); | |
| 194 | + | |
| 195 | + // 2. 更新连接和轮询任务 | |
| 196 | + for (IotModbusDeviceConfigRespDTO config : configs) { | |
| 197 | + try { | |
| 198 | + // 2.1 确保连接存在 | |
| 199 | + connectionManager.ensureConnection(config); | |
| 200 | + // 2.2 更新轮询任务 | |
| 201 | + pollScheduler.updatePolling(config); | |
| 202 | + } catch (Exception e) { | |
| 203 | + log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); | |
| 204 | + } | |
| 205 | + } | |
| 206 | + | |
| 207 | + // 3. 清理已删除设备的资源 | |
| 208 | + Set<Long> removedDeviceIds = configCacheService.cleanupRemovedDevices(configs); | |
| 209 | + for (Long deviceId : removedDeviceIds) { | |
| 210 | + pollScheduler.stopPolling(deviceId); | |
| 211 | + connectionManager.removeDevice(deviceId); | |
| 212 | + } | |
| 213 | + } catch (Exception e) { | |
| 214 | + log.error("[refreshConfig][刷新配置失败]", e); | |
| 215 | + } | |
| 216 | + } | |
| 217 | + | |
| 218 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.ObjUtil; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager; | |
| 12 | +import lombok.RequiredArgsConstructor; | |
| 13 | +import lombok.extern.slf4j.Slf4j; | |
| 14 | + | |
| 15 | +import java.util.Map; | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * IoT Modbus TCP Client 下行消息处理器 | |
| 19 | + * <p> | |
| 20 | + * 负责: | |
| 21 | + * 1. 处理下行消息(如属性设置 thing.service.property.set) | |
| 22 | + * 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备 | |
| 23 | + * | |
| 24 | + * @author 芋道源码 | |
| 25 | + */ | |
| 26 | +@RequiredArgsConstructor | |
| 27 | +@Slf4j | |
| 28 | +public class IotModbusTcpClientDownstreamHandler { | |
| 29 | + | |
| 30 | + private final IotModbusTcpClientConnectionManager connectionManager; | |
| 31 | + private final IotModbusTcpClientConfigCacheService configCacheService; | |
| 32 | + | |
| 33 | + /** | |
| 34 | + * 处理下行消息 | |
| 35 | + */ | |
| 36 | + @SuppressWarnings({"unchecked", "DuplicatedCode"}) | |
| 37 | + public void handle(IotDeviceMessage message) { | |
| 38 | + // 1.1 检查是否是属性设置消息 | |
| 39 | + if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) { | |
| 40 | + return; | |
| 41 | + } | |
| 42 | + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { | |
| 43 | + log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod()); | |
| 44 | + return; | |
| 45 | + } | |
| 46 | + // 1.2 获取设备配置 | |
| 47 | + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId()); | |
| 48 | + if (config == null) { | |
| 49 | + log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId()); | |
| 50 | + return; | |
| 51 | + } | |
| 52 | + | |
| 53 | + // 2. 解析属性值并写入 | |
| 54 | + Object params = message.getParams(); | |
| 55 | + if (!(params instanceof Map)) { | |
| 56 | + log.warn("[handle][params 不是 Map 类型: {}]", params); | |
| 57 | + return; | |
| 58 | + } | |
| 59 | + Map<String, Object> propertyMap = (Map<String, Object>) params; | |
| 60 | + for (Map.Entry<String, Object> entry : propertyMap.entrySet()) { | |
| 61 | + String identifier = entry.getKey(); | |
| 62 | + Object value = entry.getValue(); | |
| 63 | + // 2.1 查找对应的点位配置 | |
| 64 | + IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier); | |
| 65 | + if (point == null) { | |
| 66 | + log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); | |
| 67 | + continue; | |
| 68 | + } | |
| 69 | + // 2.2 检查是否支持写操作 | |
| 70 | + if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) { | |
| 71 | + log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); | |
| 72 | + continue; | |
| 73 | + } | |
| 74 | + | |
| 75 | + // 2.3 执行写入 | |
| 76 | + writeProperty(config, point, value); | |
| 77 | + } | |
| 78 | + } | |
| 79 | + | |
| 80 | + /** | |
| 81 | + * 写入属性值 | |
| 82 | + */ | |
| 83 | + private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) { | |
| 84 | + // 1.1 获取连接 | |
| 85 | + IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); | |
| 86 | + if (connection == null) { | |
| 87 | + log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId()); | |
| 88 | + return; | |
| 89 | + } | |
| 90 | + // 1.2 获取 slave ID | |
| 91 | + Integer slaveId = connectionManager.getSlaveId(config.getDeviceId()); | |
| 92 | + if (slaveId == null) { | |
| 93 | + log.warn("[writeProperty][设备 {} 没有 slaveId]", config.getDeviceId()); | |
| 94 | + return; | |
| 95 | + } | |
| 96 | + | |
| 97 | + // 2.1 转换属性值为原始值 | |
| 98 | + int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point); | |
| 99 | + // 2.2 执行 Modbus 写入 | |
| 100 | + IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues) | |
| 101 | + .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", | |
| 102 | + config.getDeviceId(), point.getIdentifier(), value)) | |
| 103 | + .onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]", | |
| 104 | + config.getDeviceId(), point.getIdentifier(), e)); | |
| 105 | + } | |
| 106 | + | |
| 107 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamSubscriber.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol; | |
| 7 | +import lombok.extern.slf4j.Slf4j; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Slf4j | |
| 15 | +public class IotModbusTcpClientDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { | |
| 16 | + | |
| 17 | + private final IotModbusTcpClientDownstreamHandler downstreamHandler; | |
| 18 | + | |
| 19 | + public IotModbusTcpClientDownstreamSubscriber(IotModbusTcpClientProtocol protocol, | |
| 20 | + IotModbusTcpClientDownstreamHandler downstreamHandler, | |
| 21 | + IotMessageBus messageBus) { | |
| 22 | + super(protocol, messageBus); | |
| 23 | + this.downstreamHandler = downstreamHandler; | |
| 24 | + } | |
| 25 | + | |
| 26 | + @Override | |
| 27 | + protected void handleMessage(IotDeviceMessage message) { | |
| 28 | + downstreamHandler.handle(message); | |
| 29 | + } | |
| 30 | + | |
| 31 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/upstream/IotModbusTcpClientUpstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream; | |
| 2 | + | |
| 3 | +import cn.hutool.core.map.MapUtil; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 10 | +import lombok.extern.slf4j.Slf4j; | |
| 11 | + | |
| 12 | +import java.util.Map; | |
| 13 | + | |
| 14 | +/** | |
| 15 | + * IoT Modbus TCP 上行数据处理器:将原始值转换为物模型属性值并上报 | |
| 16 | + * | |
| 17 | + * @author 芋道源码 | |
| 18 | + */ | |
| 19 | +@Slf4j | |
| 20 | +public class IotModbusTcpClientUpstreamHandler { | |
| 21 | + | |
| 22 | + private final IotDeviceMessageService messageService; | |
| 23 | + | |
| 24 | + private final String serverId; | |
| 25 | + | |
| 26 | + public IotModbusTcpClientUpstreamHandler(IotDeviceMessageService messageService, | |
| 27 | + String serverId) { | |
| 28 | + this.messageService = messageService; | |
| 29 | + this.serverId = serverId; | |
| 30 | + } | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 处理 Modbus 读取结果 | |
| 34 | + * | |
| 35 | + * @param config 设备配置 | |
| 36 | + * @param point 点位配置 | |
| 37 | + * @param rawValue 原始值(int 数组) | |
| 38 | + */ | |
| 39 | + public void handleReadResult(IotModbusDeviceConfigRespDTO config, | |
| 40 | + IotModbusPointRespDTO point, | |
| 41 | + int[] rawValue) { | |
| 42 | + try { | |
| 43 | + // 1.1 转换原始值为物模型属性值(点位翻译) | |
| 44 | + Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValue, point); | |
| 45 | + log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]", | |
| 46 | + config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue); | |
| 47 | + // 1.2 构造属性上报消息 | |
| 48 | + Map<String, Object> params = MapUtil.of(point.getIdentifier(), convertedValue); | |
| 49 | + IotDeviceMessage message = IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); | |
| 50 | + | |
| 51 | + // 2. 发送到消息总线 | |
| 52 | + messageService.sendDeviceMessage(message, config.getProductKey(), | |
| 53 | + config.getDeviceName(), serverId); | |
| 54 | + } catch (Exception e) { | |
| 55 | + log.error("[handleReadResult][处理读取结果失败, deviceId={}, identifier={}]", | |
| 56 | + config.getDeviceId(), point.getIdentifier(), e); | |
| 57 | + } | |
| 58 | + } | |
| 59 | + | |
| 60 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConfigCacheService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | |
| 4 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 10 | +import lombok.RequiredArgsConstructor; | |
| 11 | +import lombok.extern.slf4j.Slf4j; | |
| 12 | + | |
| 13 | +import java.util.HashSet; | |
| 14 | +import java.util.List; | |
| 15 | +import java.util.Map; | |
| 16 | +import java.util.Set; | |
| 17 | +import java.util.concurrent.ConcurrentHashMap; | |
| 18 | + | |
| 19 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; | |
| 20 | + | |
| 21 | +/** | |
| 22 | + * IoT Modbus TCP Client 配置缓存服务 | |
| 23 | + * | |
| 24 | + * @author 芋道源码 | |
| 25 | + */ | |
| 26 | +@RequiredArgsConstructor | |
| 27 | +@Slf4j | |
| 28 | +public class IotModbusTcpClientConfigCacheService { | |
| 29 | + | |
| 30 | + private final IotDeviceCommonApi deviceApi; | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 配置缓存:deviceId -> 配置 | |
| 34 | + */ | |
| 35 | + private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>(); | |
| 36 | + | |
| 37 | + /** | |
| 38 | + * 已知的设备 ID 集合(作用:用于检测已删除的设备) | |
| 39 | + * | |
| 40 | + * @see #cleanupRemovedDevices(List) | |
| 41 | + */ | |
| 42 | + private final Set<Long> knownDeviceIds = ConcurrentHashMap.newKeySet(); | |
| 43 | + | |
| 44 | + /** | |
| 45 | + * 刷新配置 | |
| 46 | + * | |
| 47 | + * @return 最新的配置列表;API 失败时返回 null(调用方应跳过 cleanup) | |
| 48 | + */ | |
| 49 | + public List<IotModbusDeviceConfigRespDTO> refreshConfig() { | |
| 50 | + try { | |
| 51 | + // 1. 从远程获取配置 | |
| 52 | + CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList( | |
| 53 | + new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus()) | |
| 54 | + .setMode(IotModbusModeEnum.POLLING.getMode()).setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_CLIENT.getType())); | |
| 55 | + result.checkError(); | |
| 56 | + List<IotModbusDeviceConfigRespDTO> configs = result.getData(); | |
| 57 | + | |
| 58 | + // 2. 更新缓存(注意:不在这里更新 knownDeviceIds,由 cleanupRemovedDevices 统一管理) | |
| 59 | + for (IotModbusDeviceConfigRespDTO config : configs) { | |
| 60 | + configCache.put(config.getDeviceId(), config); | |
| 61 | + } | |
| 62 | + return configs; | |
| 63 | + } catch (Exception e) { | |
| 64 | + log.error("[refreshConfig][刷新配置失败]", e); | |
| 65 | + return null; | |
| 66 | + } | |
| 67 | + } | |
| 68 | + | |
| 69 | + /** | |
| 70 | + * 获取设备配置 | |
| 71 | + * | |
| 72 | + * @param deviceId 设备 ID | |
| 73 | + * @return 配置 | |
| 74 | + */ | |
| 75 | + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { | |
| 76 | + return configCache.get(deviceId); | |
| 77 | + } | |
| 78 | + | |
| 79 | + /** | |
| 80 | + * 计算已删除设备的 ID 集合,清理缓存,并更新已知设备 ID 集合 | |
| 81 | + * | |
| 82 | + * @param currentConfigs 当前有效的配置列表 | |
| 83 | + * @return 已删除的设备 ID 集合 | |
| 84 | + */ | |
| 85 | + public Set<Long> cleanupRemovedDevices(List<IotModbusDeviceConfigRespDTO> currentConfigs) { | |
| 86 | + // 1.1 获取当前有效的设备 ID | |
| 87 | + Set<Long> currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId); | |
| 88 | + // 1.2 找出已删除的设备(基于旧的 knownDeviceIds) | |
| 89 | + Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds); | |
| 90 | + removedDeviceIds.removeAll(currentDeviceIds); | |
| 91 | + | |
| 92 | + // 2. 清理已删除设备的缓存 | |
| 93 | + for (Long deviceId : removedDeviceIds) { | |
| 94 | + log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); | |
| 95 | + configCache.remove(deviceId); | |
| 96 | + } | |
| 97 | + | |
| 98 | + // 3. 更新已知设备 ID 集合为当前有效的设备 ID | |
| 99 | + knownDeviceIds.clear(); | |
| 100 | + knownDeviceIds.addAll(currentDeviceIds); | |
| 101 | + return removedDeviceIds; | |
| 102 | + } | |
| 103 | + | |
| 104 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConnectionManager.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.ObjUtil; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 7 | +import com.ghgande.j2mod.modbus.net.TCPMasterConnection; | |
| 8 | +import io.vertx.core.Context; | |
| 9 | +import io.vertx.core.Future; | |
| 10 | +import io.vertx.core.Vertx; | |
| 11 | +import lombok.Data; | |
| 12 | +import lombok.extern.slf4j.Slf4j; | |
| 13 | +import org.redisson.api.RLock; | |
| 14 | +import org.redisson.api.RedissonClient; | |
| 15 | + | |
| 16 | +import java.net.InetAddress; | |
| 17 | +import java.util.ArrayList; | |
| 18 | +import java.util.List; | |
| 19 | +import java.util.Map; | |
| 20 | +import java.util.concurrent.ConcurrentHashMap; | |
| 21 | + | |
| 22 | +/** | |
| 23 | + * IoT Modbus TCP 连接管理器 | |
| 24 | + * <p> | |
| 25 | + * 统一管理 Modbus TCP 连接: | |
| 26 | + * 1. 管理 TCP 连接(相同 ip:port 共用连接) | |
| 27 | + * 2. 分布式锁管理(连接级别),避免多节点重复创建连接 | |
| 28 | + * 3. 连接重试和故障恢复 | |
| 29 | + * | |
| 30 | + * @author 芋道源码 | |
| 31 | + */ | |
| 32 | +@Slf4j | |
| 33 | +public class IotModbusTcpClientConnectionManager { | |
| 34 | + | |
| 35 | + private static final String LOCK_KEY_PREFIX = "iot:modbus-tcp:connection:"; | |
| 36 | + | |
| 37 | + private final RedissonClient redissonClient; | |
| 38 | + private final Vertx vertx; | |
| 39 | + private final IotDeviceMessageService messageService; | |
| 40 | + private final IotModbusTcpClientConfigCacheService configCacheService; | |
| 41 | + private final String serverId; | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * 连接池:key = ip:port | |
| 45 | + */ | |
| 46 | + private final Map<String, ModbusConnection> connectionPool = new ConcurrentHashMap<>(); | |
| 47 | + | |
| 48 | + /** | |
| 49 | + * 设备 ID 到连接 key 的映射 | |
| 50 | + */ | |
| 51 | + private final Map<Long, String> deviceConnectionMap = new ConcurrentHashMap<>(); | |
| 52 | + | |
| 53 | + public IotModbusTcpClientConnectionManager(RedissonClient redissonClient, Vertx vertx, | |
| 54 | + IotDeviceMessageService messageService, | |
| 55 | + IotModbusTcpClientConfigCacheService configCacheService, | |
| 56 | + String serverId) { | |
| 57 | + this.redissonClient = redissonClient; | |
| 58 | + this.vertx = vertx; | |
| 59 | + this.messageService = messageService; | |
| 60 | + this.configCacheService = configCacheService; | |
| 61 | + this.serverId = serverId; | |
| 62 | + } | |
| 63 | + | |
| 64 | + /** | |
| 65 | + * 确保连接存在 | |
| 66 | + * <p> | |
| 67 | + * 首次建连成功时,直接发送设备上线消息 | |
| 68 | + * | |
| 69 | + * @param config 设备配置 | |
| 70 | + */ | |
| 71 | + public void ensureConnection(IotModbusDeviceConfigRespDTO config) { | |
| 72 | + // 1.1 检查设备是否切换了 IP/端口,若是则先清理旧连接 | |
| 73 | + String connectionKey = buildConnectionKey(config.getIp(), config.getPort()); | |
| 74 | + String oldConnectionKey = deviceConnectionMap.get(config.getDeviceId()); | |
| 75 | + if (oldConnectionKey != null && ObjUtil.notEqual(oldConnectionKey, connectionKey)) { | |
| 76 | + log.info("[ensureConnection][设备 {} IP/端口变更: {} -> {}, 清理旧连接]", | |
| 77 | + config.getDeviceId(), oldConnectionKey, connectionKey); | |
| 78 | + removeDevice(config.getDeviceId()); | |
| 79 | + } | |
| 80 | + // 1.2 记录设备与连接的映射 | |
| 81 | + deviceConnectionMap.put(config.getDeviceId(), connectionKey); | |
| 82 | + | |
| 83 | + // 2. 情况一:连接已存在,注册设备并发送上线消息 | |
| 84 | + ModbusConnection connection = connectionPool.get(connectionKey); | |
| 85 | + if (connection != null) { | |
| 86 | + addDeviceAndOnline(connection, config); | |
| 87 | + return; | |
| 88 | + } | |
| 89 | + | |
| 90 | + // 3. 情况二:连接不存在,加分布式锁创建新连接 | |
| 91 | + RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + connectionKey); | |
| 92 | + if (!lock.tryLock()) { | |
| 93 | + log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey); | |
| 94 | + return; | |
| 95 | + } | |
| 96 | + try { | |
| 97 | + // 3.1 double-check:拿到锁后再次检查,避免并发创建重复连接 | |
| 98 | + connection = connectionPool.get(connectionKey); | |
| 99 | + if (connection != null) { | |
| 100 | + addDeviceAndOnline(connection, config); | |
| 101 | + lock.unlock(); | |
| 102 | + return; | |
| 103 | + } | |
| 104 | + // 3.2 创建新连接 | |
| 105 | + connection = createConnection(config); | |
| 106 | + connection.setLock(lock); | |
| 107 | + connectionPool.put(connectionKey, connection); | |
| 108 | + log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey); | |
| 109 | + // 3.3 注册设备并发送上线消息 | |
| 110 | + addDeviceAndOnline(connection, config); | |
| 111 | + } catch (Exception e) { | |
| 112 | + log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e); | |
| 113 | + // 建连失败,释放锁让其他节点可重试 | |
| 114 | + lock.unlock(); | |
| 115 | + } | |
| 116 | + } | |
| 117 | + | |
| 118 | + /** | |
| 119 | + * 创建 Modbus TCP 连接 | |
| 120 | + */ | |
| 121 | + private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config) throws Exception { | |
| 122 | + // 1. 创建 TCP 连接 | |
| 123 | + TCPMasterConnection tcpConnection = new TCPMasterConnection(InetAddress.getByName(config.getIp())); | |
| 124 | + tcpConnection.setPort(config.getPort()); | |
| 125 | + tcpConnection.setTimeout(config.getTimeout()); | |
| 126 | + tcpConnection.connect(); | |
| 127 | + | |
| 128 | + // 2. 创建 Modbus 连接对象 | |
| 129 | + return new ModbusConnection() | |
| 130 | + .setConnectionKey(buildConnectionKey(config.getIp(), config.getPort())) | |
| 131 | + .setTcpConnection(tcpConnection).setContext(vertx.getOrCreateContext()) | |
| 132 | + .setTimeout(config.getTimeout()).setRetryInterval(config.getRetryInterval()); | |
| 133 | + } | |
| 134 | + | |
| 135 | + /** | |
| 136 | + * 获取连接 | |
| 137 | + */ | |
| 138 | + public ModbusConnection getConnection(Long deviceId) { | |
| 139 | + String connectionKey = deviceConnectionMap.get(deviceId); | |
| 140 | + if (connectionKey == null) { | |
| 141 | + return null; | |
| 142 | + } | |
| 143 | + return connectionPool.get(connectionKey); | |
| 144 | + } | |
| 145 | + | |
| 146 | + /** | |
| 147 | + * 获取设备的 slave ID | |
| 148 | + */ | |
| 149 | + public Integer getSlaveId(Long deviceId) { | |
| 150 | + ModbusConnection connection = getConnection(deviceId); | |
| 151 | + if (connection == null) { | |
| 152 | + return null; | |
| 153 | + } | |
| 154 | + return connection.getSlaveId(deviceId); | |
| 155 | + } | |
| 156 | + | |
| 157 | + /** | |
| 158 | + * 移除设备 | |
| 159 | + * <p> | |
| 160 | + * 移除时直接发送设备下线消息 | |
| 161 | + */ | |
| 162 | + public void removeDevice(Long deviceId) { | |
| 163 | + // 1.1 移除设备时,发送下线消息 | |
| 164 | + sendOfflineMessage(deviceId); | |
| 165 | + // 1.2 移除设备引用 | |
| 166 | + String connectionKey = deviceConnectionMap.remove(deviceId); | |
| 167 | + if (connectionKey == null) { | |
| 168 | + return; | |
| 169 | + } | |
| 170 | + | |
| 171 | + // 2.1 移除连接中的设备引用 | |
| 172 | + ModbusConnection connection = connectionPool.get(connectionKey); | |
| 173 | + if (connection == null) { | |
| 174 | + return; | |
| 175 | + } | |
| 176 | + connection.removeDevice(deviceId); | |
| 177 | + // 2.2 如果没有设备引用了,关闭连接 | |
| 178 | + if (connection.getDeviceCount() == 0) { | |
| 179 | + closeConnection(connectionKey); | |
| 180 | + } | |
| 181 | + } | |
| 182 | + | |
| 183 | + // ==================== 设备连接 & 上下线消息 ==================== | |
| 184 | + | |
| 185 | + /** | |
| 186 | + * 注册设备到连接,并发送上线消息 | |
| 187 | + */ | |
| 188 | + private void addDeviceAndOnline(ModbusConnection connection, | |
| 189 | + IotModbusDeviceConfigRespDTO config) { | |
| 190 | + Integer previous = connection.addDevice(config.getDeviceId(), config.getSlaveId()); | |
| 191 | + // 首次注册,发送上线消息 | |
| 192 | + if (previous == null) { | |
| 193 | + sendOnlineMessage(config); | |
| 194 | + } | |
| 195 | + } | |
| 196 | + | |
| 197 | + /** | |
| 198 | + * 发送设备上线消息 | |
| 199 | + */ | |
| 200 | + private void sendOnlineMessage(IotModbusDeviceConfigRespDTO config) { | |
| 201 | + try { | |
| 202 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 203 | + messageService.sendDeviceMessage(onlineMessage, | |
| 204 | + config.getProductKey(), config.getDeviceName(), serverId); | |
| 205 | + } catch (Exception ex) { | |
| 206 | + log.error("[sendOnlineMessage][发送设备上线消息失败, deviceId={}]", config.getDeviceId(), ex); | |
| 207 | + } | |
| 208 | + } | |
| 209 | + | |
| 210 | + /** | |
| 211 | + * 发送设备下线消息 | |
| 212 | + */ | |
| 213 | + private void sendOfflineMessage(Long deviceId) { | |
| 214 | + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); | |
| 215 | + if (config == null) { | |
| 216 | + return; | |
| 217 | + } | |
| 218 | + try { | |
| 219 | + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 220 | + messageService.sendDeviceMessage(offlineMessage, | |
| 221 | + config.getProductKey(), config.getDeviceName(), serverId); | |
| 222 | + } catch (Exception ex) { | |
| 223 | + log.error("[sendOfflineMessage][发送设备下线消息失败, deviceId={}]", deviceId, ex); | |
| 224 | + } | |
| 225 | + } | |
| 226 | + | |
| 227 | + /** | |
| 228 | + * 关闭指定连接 | |
| 229 | + */ | |
| 230 | + private void closeConnection(String connectionKey) { | |
| 231 | + ModbusConnection connection = connectionPool.remove(connectionKey); | |
| 232 | + if (connection == null) { | |
| 233 | + return; | |
| 234 | + } | |
| 235 | + | |
| 236 | + try { | |
| 237 | + if (connection.getTcpConnection() != null) { | |
| 238 | + connection.getTcpConnection().close(); | |
| 239 | + } | |
| 240 | + // 释放分布式锁,让其他节点可接管 | |
| 241 | + RLock lock = connection.getLock(); | |
| 242 | + if (lock != null && lock.isHeldByCurrentThread()) { | |
| 243 | + lock.unlock(); | |
| 244 | + } | |
| 245 | + log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey); | |
| 246 | + } catch (Exception e) { | |
| 247 | + log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e); | |
| 248 | + } | |
| 249 | + } | |
| 250 | + | |
| 251 | + /** | |
| 252 | + * 关闭所有连接 | |
| 253 | + */ | |
| 254 | + public void closeAll() { | |
| 255 | + // 先复制再遍历,避免 closeConnection 中 remove 导致并发修改 | |
| 256 | + List<String> connectionKeys = new ArrayList<>(connectionPool.keySet()); | |
| 257 | + for (String connectionKey : connectionKeys) { | |
| 258 | + closeConnection(connectionKey); | |
| 259 | + } | |
| 260 | + deviceConnectionMap.clear(); | |
| 261 | + } | |
| 262 | + | |
| 263 | + private String buildConnectionKey(String ip, Integer port) { | |
| 264 | + return ip + ":" + port; | |
| 265 | + } | |
| 266 | + | |
| 267 | + /** | |
| 268 | + * Modbus 连接信息 | |
| 269 | + */ | |
| 270 | + @Data | |
| 271 | + public static class ModbusConnection { | |
| 272 | + | |
| 273 | + private String connectionKey; | |
| 274 | + private TCPMasterConnection tcpConnection; | |
| 275 | + private Integer timeout; | |
| 276 | + private Integer retryInterval; | |
| 277 | + /** | |
| 278 | + * 设备 ID 到 slave ID 的映射 | |
| 279 | + */ | |
| 280 | + private final Map<Long, Integer> deviceSlaveMap = new ConcurrentHashMap<>(); | |
| 281 | + | |
| 282 | + /** | |
| 283 | + * 分布式锁,锁住连接的创建和销毁,避免多节点重复连接同一从站 | |
| 284 | + */ | |
| 285 | + private RLock lock; | |
| 286 | + | |
| 287 | + /** | |
| 288 | + * Vert.x Context,用于 executeBlocking 执行 Modbus 操作,保证同一连接的操作串行执行 | |
| 289 | + */ | |
| 290 | + private Context context; | |
| 291 | + | |
| 292 | + public Integer addDevice(Long deviceId, Integer slaveId) { | |
| 293 | + return deviceSlaveMap.putIfAbsent(deviceId, slaveId); | |
| 294 | + } | |
| 295 | + | |
| 296 | + public void removeDevice(Long deviceId) { | |
| 297 | + deviceSlaveMap.remove(deviceId); | |
| 298 | + } | |
| 299 | + | |
| 300 | + public int getDeviceCount() { | |
| 301 | + return deviceSlaveMap.size(); | |
| 302 | + } | |
| 303 | + | |
| 304 | + public Integer getSlaveId(Long deviceId) { | |
| 305 | + return deviceSlaveMap.get(deviceId); | |
| 306 | + } | |
| 307 | + | |
| 308 | + /** | |
| 309 | + * 执行 Modbus 读取操作(阻塞方式,在 Vert.x worker 线程执行) | |
| 310 | + */ | |
| 311 | + public <T> Future<T> executeBlocking(java.util.function.Function<TCPMasterConnection, T> operation) { | |
| 312 | + // ordered=true 保证同一 Context 的操作串行执行,不同连接之间可并行 | |
| 313 | + return context.executeBlocking(() -> operation.apply(tcpConnection), true); | |
| 314 | + } | |
| 315 | + } | |
| 316 | + | |
| 317 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.lang.Assert; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler; | |
| 11 | +import io.vertx.core.Vertx; | |
| 12 | +import lombok.extern.slf4j.Slf4j; | |
| 13 | + | |
| 14 | +/** | |
| 15 | + * IoT Modbus TCP Client 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 | |
| 16 | + * | |
| 17 | + * @author 芋道源码 | |
| 18 | + */ | |
| 19 | +@Slf4j | |
| 20 | +public class IotModbusTcpClientPollScheduler extends AbstractIotModbusPollScheduler { | |
| 21 | + | |
| 22 | + private final IotModbusTcpClientConnectionManager connectionManager; | |
| 23 | + private final IotModbusTcpClientUpstreamHandler upstreamHandler; | |
| 24 | + private final IotModbusTcpClientConfigCacheService configCacheService; | |
| 25 | + | |
| 26 | + public IotModbusTcpClientPollScheduler(Vertx vertx, | |
| 27 | + IotModbusTcpClientConnectionManager connectionManager, | |
| 28 | + IotModbusTcpClientUpstreamHandler upstreamHandler, | |
| 29 | + IotModbusTcpClientConfigCacheService configCacheService) { | |
| 30 | + super(vertx); | |
| 31 | + this.connectionManager = connectionManager; | |
| 32 | + this.upstreamHandler = upstreamHandler; | |
| 33 | + this.configCacheService = configCacheService; | |
| 34 | + } | |
| 35 | + | |
| 36 | + // ========== 轮询执行 ========== | |
| 37 | + | |
| 38 | + /** | |
| 39 | + * 轮询单个点位 | |
| 40 | + */ | |
| 41 | + @Override | |
| 42 | + protected void pollPoint(Long deviceId, Long pointId) { | |
| 43 | + // 1.1 从 configCache 获取最新配置 | |
| 44 | + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); | |
| 45 | + if (config == null || CollUtil.isEmpty(config.getPoints())) { | |
| 46 | + log.warn("[pollPoint][设备 {} 没有配置]", deviceId); | |
| 47 | + return; | |
| 48 | + } | |
| 49 | + // 1.2 查找点位 | |
| 50 | + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId); | |
| 51 | + if (point == null) { | |
| 52 | + log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId); | |
| 53 | + return; | |
| 54 | + } | |
| 55 | + | |
| 56 | + // 2.1 获取连接 | |
| 57 | + IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId); | |
| 58 | + if (connection == null) { | |
| 59 | + log.warn("[pollPoint][设备 {} 没有连接]", deviceId); | |
| 60 | + return; | |
| 61 | + } | |
| 62 | + // 2.2 获取 slave ID | |
| 63 | + Integer slaveId = connectionManager.getSlaveId(deviceId); | |
| 64 | + Assert.notNull(slaveId, "设备 {} 没有配置 slaveId", deviceId); | |
| 65 | + | |
| 66 | + // 3. 执行 Modbus 读取 | |
| 67 | + IotModbusTcpClientUtils.read(connection, slaveId, point) | |
| 68 | + .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue)) | |
| 69 | + .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]", | |
| 70 | + deviceId, point.getIdentifier(), e)); | |
| 71 | + } | |
| 72 | + | |
| 73 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/package-info.java
0 → 100644
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerConfig.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; | |
| 2 | + | |
| 3 | +import jakarta.validation.constraints.Max; | |
| 4 | +import jakarta.validation.constraints.Min; | |
| 5 | +import jakarta.validation.constraints.NotNull; | |
| 6 | +import lombok.Data; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * IoT Modbus TCP Server 协议配置 | |
| 10 | + * | |
| 11 | + * @author 芋道源码 | |
| 12 | + */ | |
| 13 | +@Data | |
| 14 | +public class IotModbusTcpServerConfig { | |
| 15 | + | |
| 16 | + /** | |
| 17 | + * 配置刷新间隔(秒) | |
| 18 | + */ | |
| 19 | + @NotNull(message = "配置刷新间隔不能为空") | |
| 20 | + @Min(value = 1, message = "配置刷新间隔不能小于 1 秒") | |
| 21 | + private Integer configRefreshInterval = 30; | |
| 22 | + | |
| 23 | + /** | |
| 24 | + * 自定义功能码(用于认证等扩展交互) | |
| 25 | + * Modbus 协议保留 65-72 给用户自定义,默认 65 | |
| 26 | + */ | |
| 27 | + @NotNull(message = "自定义功能码不能为空") | |
| 28 | + @Min(value = 65, message = "自定义功能码不能小于 65") | |
| 29 | + @Max(value = 72, message = "自定义功能码不能大于 72") | |
| 30 | + private Integer customFunctionCode = 65; | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * Pending Request 超时时间(毫秒) | |
| 34 | + */ | |
| 35 | + @NotNull(message = "请求超时时间不能为空") | |
| 36 | + private Integer requestTimeout = 5000; | |
| 37 | + | |
| 38 | + /** | |
| 39 | + * Pending Request 清理间隔(毫秒) | |
| 40 | + */ | |
| 41 | + @NotNull(message = "请求清理间隔不能为空") | |
| 42 | + private Integer requestCleanupInterval = 10000; | |
| 43 | + | |
| 44 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.lang.Assert; | |
| 5 | +import cn.hutool.extra.spring.SpringUtil; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 10 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 11 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder; | |
| 15 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; | |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamHandler; | |
| 17 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamSubscriber; | |
| 18 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.upstream.IotModbusTcpServerUpstreamHandler; | |
| 19 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService; | |
| 20 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager; | |
| 21 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo; | |
| 22 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager; | |
| 23 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPollScheduler; | |
| 24 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 25 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 26 | +import io.vertx.core.Vertx; | |
| 27 | +import io.vertx.core.net.NetServer; | |
| 28 | +import io.vertx.core.net.NetServerOptions; | |
| 29 | +import io.vertx.core.net.NetSocket; | |
| 30 | +import io.vertx.core.parsetools.RecordParser; | |
| 31 | +import lombok.Getter; | |
| 32 | +import lombok.extern.slf4j.Slf4j; | |
| 33 | + | |
| 34 | +import java.util.List; | |
| 35 | +import java.util.Set; | |
| 36 | +import java.util.concurrent.TimeUnit; | |
| 37 | +import java.util.concurrent.atomic.AtomicInteger; | |
| 38 | + | |
| 39 | +/** | |
| 40 | + * IoT 网关 Modbus TCP Server 协议 | |
| 41 | + * <p> | |
| 42 | + * 作为 TCP Server 接收设备主动连接: | |
| 43 | + * 1. 设备通过自定义功能码(FC 65)发送认证请求 | |
| 44 | + * 2. 认证成功后,网关主动发送 Modbus 读请求,设备响应(云端轮询模式) | |
| 45 | + * | |
| 46 | + * @author 芋道源码 | |
| 47 | + */ | |
| 48 | +@Slf4j | |
| 49 | +public class IotModbusTcpServerProtocol implements IotProtocol { | |
| 50 | + | |
| 51 | + /** | |
| 52 | + * 协议配置 | |
| 53 | + */ | |
| 54 | + private final ProtocolProperties properties; | |
| 55 | + /** | |
| 56 | + * 服务器 ID(用于消息追踪,全局唯一) | |
| 57 | + */ | |
| 58 | + @Getter | |
| 59 | + private final String serverId; | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * 运行状态 | |
| 63 | + */ | |
| 64 | + @Getter | |
| 65 | + private volatile boolean running = false; | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * Vert.x 实例 | |
| 69 | + */ | |
| 70 | + private final Vertx vertx; | |
| 71 | + /** | |
| 72 | + * TCP Server | |
| 73 | + */ | |
| 74 | + private NetServer netServer; | |
| 75 | + /** | |
| 76 | + * 配置刷新定时器 ID | |
| 77 | + */ | |
| 78 | + private Long configRefreshTimerId; | |
| 79 | + /** | |
| 80 | + * Pending Request 清理定时器 ID | |
| 81 | + */ | |
| 82 | + private Long requestCleanupTimerId; | |
| 83 | + | |
| 84 | + /** | |
| 85 | + * 连接管理器 | |
| 86 | + */ | |
| 87 | + private final IotModbusTcpServerConnectionManager connectionManager; | |
| 88 | + /** | |
| 89 | + * 下行消息订阅者 | |
| 90 | + */ | |
| 91 | + private IotModbusTcpServerDownstreamSubscriber downstreamSubscriber; | |
| 92 | + | |
| 93 | + private final IotModbusFrameDecoder frameDecoder; | |
| 94 | + @SuppressWarnings("FieldCanBeLocal") | |
| 95 | + private final IotModbusFrameEncoder frameEncoder; | |
| 96 | + | |
| 97 | + private final IotModbusTcpServerConfigCacheService configCacheService; | |
| 98 | + private final IotModbusTcpServerPendingRequestManager pendingRequestManager; | |
| 99 | + private final IotModbusTcpServerUpstreamHandler upstreamHandler; | |
| 100 | + private final IotModbusTcpServerPollScheduler pollScheduler; | |
| 101 | + private final IotDeviceMessageService messageService; | |
| 102 | + | |
| 103 | + public IotModbusTcpServerProtocol(ProtocolProperties properties) { | |
| 104 | + IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer(); | |
| 105 | + Assert.notNull(slaveConfig, "Modbus TCP Server 协议配置(modbusTcpServer)不能为空"); | |
| 106 | + this.properties = properties; | |
| 107 | + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | |
| 108 | + | |
| 109 | + // 初始化 Vertx | |
| 110 | + this.vertx = Vertx.vertx(); | |
| 111 | + | |
| 112 | + // 初始化 Manager | |
| 113 | + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 114 | + this.connectionManager = new IotModbusTcpServerConnectionManager(); | |
| 115 | + this.configCacheService = new IotModbusTcpServerConfigCacheService(deviceApi); | |
| 116 | + this.pendingRequestManager = new IotModbusTcpServerPendingRequestManager(); | |
| 117 | + | |
| 118 | + // 初始化帧编解码器 | |
| 119 | + this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode()); | |
| 120 | + this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode()); | |
| 121 | + | |
| 122 | + // 初始化共享事务 ID 自增器(PollScheduler 和 DownstreamHandler 共用,避免 transactionId 冲突) | |
| 123 | + AtomicInteger transactionIdCounter = new AtomicInteger(0); | |
| 124 | + // 初始化轮询调度器 | |
| 125 | + this.pollScheduler = new IotModbusTcpServerPollScheduler( | |
| 126 | + vertx, connectionManager, frameEncoder, pendingRequestManager, | |
| 127 | + slaveConfig.getRequestTimeout(), transactionIdCounter, configCacheService); | |
| 128 | + | |
| 129 | + // 初始化 Handler | |
| 130 | + this.messageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 131 | + IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); | |
| 132 | + this.upstreamHandler = new IotModbusTcpServerUpstreamHandler( | |
| 133 | + deviceApi, this.messageService, frameEncoder, | |
| 134 | + connectionManager, configCacheService, pendingRequestManager, | |
| 135 | + pollScheduler, deviceService, serverId); | |
| 136 | + } | |
| 137 | + | |
| 138 | + @Override | |
| 139 | + public String getId() { | |
| 140 | + return properties.getId(); | |
| 141 | + } | |
| 142 | + | |
| 143 | + @Override | |
| 144 | + public IotProtocolTypeEnum getType() { | |
| 145 | + return IotProtocolTypeEnum.MODBUS_TCP_SERVER; | |
| 146 | + } | |
| 147 | + | |
| 148 | + @Override | |
| 149 | + public void start() { | |
| 150 | + if (running) { | |
| 151 | + log.warn("[start][IoT Modbus TCP Server 协议 {} 已经在运行中]", getId()); | |
| 152 | + return; | |
| 153 | + } | |
| 154 | + | |
| 155 | + try { | |
| 156 | + // 1. 启动配置刷新定时器 | |
| 157 | + IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer(); | |
| 158 | + configRefreshTimerId = vertx.setPeriodic( | |
| 159 | + TimeUnit.SECONDS.toMillis(slaveConfig.getConfigRefreshInterval()), | |
| 160 | + id -> refreshConfig()); | |
| 161 | + | |
| 162 | + // 2.1 启动 TCP Server | |
| 163 | + startTcpServer(); | |
| 164 | + | |
| 165 | + // 2.2 启动 PendingRequest 清理定时器 | |
| 166 | + requestCleanupTimerId = vertx.setPeriodic( | |
| 167 | + slaveConfig.getRequestCleanupInterval(), | |
| 168 | + id -> pendingRequestManager.cleanupExpired()); | |
| 169 | + running = true; | |
| 170 | + log.info("[start][IoT Modbus TCP Server 协议 {} 启动成功, serverId={}, port={}]", | |
| 171 | + getId(), serverId, properties.getPort()); | |
| 172 | + | |
| 173 | + // 3. 启动下行消息订阅 | |
| 174 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 175 | + IotModbusTcpServerDownstreamHandler downstreamHandler = new IotModbusTcpServerDownstreamHandler( | |
| 176 | + connectionManager, configCacheService, frameEncoder, this.pollScheduler.getTransactionIdCounter()); | |
| 177 | + this.downstreamSubscriber = new IotModbusTcpServerDownstreamSubscriber( | |
| 178 | + this, downstreamHandler, messageBus); | |
| 179 | + downstreamSubscriber.start(); | |
| 180 | + } catch (Exception e) { | |
| 181 | + log.error("[start][IoT Modbus TCP Server 协议 {} 启动失败]", getId(), e); | |
| 182 | + stop0(); | |
| 183 | + throw e; | |
| 184 | + } | |
| 185 | + } | |
| 186 | + | |
| 187 | + @Override | |
| 188 | + public void stop() { | |
| 189 | + if (!running) { | |
| 190 | + return; | |
| 191 | + } | |
| 192 | + stop0(); | |
| 193 | + } | |
| 194 | + | |
| 195 | + private void stop0() { | |
| 196 | + // 1. 停止下行消息订阅 | |
| 197 | + if (downstreamSubscriber != null) { | |
| 198 | + try { | |
| 199 | + downstreamSubscriber.stop(); | |
| 200 | + } catch (Exception e) { | |
| 201 | + log.error("[stop][下行消息订阅器停止失败]", e); | |
| 202 | + } | |
| 203 | + downstreamSubscriber = null; | |
| 204 | + } | |
| 205 | + | |
| 206 | + // 2.1 取消定时器 | |
| 207 | + if (configRefreshTimerId != null) { | |
| 208 | + vertx.cancelTimer(configRefreshTimerId); | |
| 209 | + configRefreshTimerId = null; | |
| 210 | + } | |
| 211 | + if (requestCleanupTimerId != null) { | |
| 212 | + vertx.cancelTimer(requestCleanupTimerId); | |
| 213 | + requestCleanupTimerId = null; | |
| 214 | + } | |
| 215 | + // 2.2 停止轮询 | |
| 216 | + pollScheduler.stopAll(); | |
| 217 | + // 2.3 清理 PendingRequest | |
| 218 | + pendingRequestManager.clear(); | |
| 219 | + // 2.4 关闭所有连接 | |
| 220 | + connectionManager.closeAll(); | |
| 221 | + // 2.5 关闭 TCP Server | |
| 222 | + if (netServer != null) { | |
| 223 | + try { | |
| 224 | + netServer.close().result(); | |
| 225 | + log.info("[stop][TCP Server 已关闭]"); | |
| 226 | + } catch (Exception e) { | |
| 227 | + log.error("[stop][TCP Server 关闭失败]", e); | |
| 228 | + } | |
| 229 | + netServer = null; | |
| 230 | + } | |
| 231 | + | |
| 232 | + // 3. 关闭 Vertx | |
| 233 | + if (vertx != null) { | |
| 234 | + try { | |
| 235 | + vertx.close().result(); | |
| 236 | + } catch (Exception e) { | |
| 237 | + log.error("[stop][Vertx 关闭失败]", e); | |
| 238 | + } | |
| 239 | + } | |
| 240 | + running = false; | |
| 241 | + log.info("[stop][IoT Modbus TCP Server 协议 {} 已停止]", getId()); | |
| 242 | + } | |
| 243 | + | |
| 244 | + /** | |
| 245 | + * 启动 TCP Server | |
| 246 | + */ | |
| 247 | + private void startTcpServer() { | |
| 248 | + // 1. 创建 TCP Server | |
| 249 | + NetServerOptions options = new NetServerOptions() | |
| 250 | + .setPort(properties.getPort()); | |
| 251 | + netServer = vertx.createNetServer(options); | |
| 252 | + | |
| 253 | + // 2. 设置连接处理器 | |
| 254 | + netServer.connectHandler(this::handleConnection); | |
| 255 | + try { | |
| 256 | + netServer.listen().toCompletionStage().toCompletableFuture().get(); | |
| 257 | + log.info("[startTcpServer][TCP Server 启动成功, port={}]", properties.getPort()); | |
| 258 | + } catch (Exception e) { | |
| 259 | + throw new RuntimeException("[startTcpServer][TCP Server 启动失败]", e); | |
| 260 | + } | |
| 261 | + } | |
| 262 | + | |
| 263 | + /** | |
| 264 | + * 处理新连接 | |
| 265 | + */ | |
| 266 | + private void handleConnection(NetSocket socket) { | |
| 267 | + log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress()); | |
| 268 | + | |
| 269 | + // 1. 创建 RecordParser 并设置为数据处理器 | |
| 270 | + RecordParser recordParser = frameDecoder.createRecordParser((frame, frameFormat) -> { | |
| 271 | + // 【重要】帧处理分发,即消息处理 | |
| 272 | + upstreamHandler.handleFrame(socket, frame, frameFormat); | |
| 273 | + }); | |
| 274 | + socket.handler(recordParser); | |
| 275 | + | |
| 276 | + // 2.1 连接关闭处理 | |
| 277 | + socket.closeHandler(v -> { | |
| 278 | + ConnectionInfo info = connectionManager.removeConnection(socket); | |
| 279 | + if (info == null || info.getDeviceId() == null) { | |
| 280 | + log.info("[handleConnection][未认证连接关闭, remoteAddress={}]", socket.remoteAddress()); | |
| 281 | + return; | |
| 282 | + } | |
| 283 | + pollScheduler.stopPolling(info.getDeviceId()); | |
| 284 | + pendingRequestManager.removeDevice(info.getDeviceId()); | |
| 285 | + configCacheService.removeConfig(info.getDeviceId()); | |
| 286 | + // 发送设备下线消息 | |
| 287 | + try { | |
| 288 | + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 289 | + messageService.sendDeviceMessage(offlineMessage, info.getProductKey(), info.getDeviceName(), serverId); | |
| 290 | + } catch (Exception ex) { | |
| 291 | + log.error("[handleConnection][发送设备下线消息失败, deviceId={}]", info.getDeviceId(), ex); | |
| 292 | + } | |
| 293 | + log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", | |
| 294 | + info.getDeviceId(), socket.remoteAddress()); | |
| 295 | + }); | |
| 296 | + // 2.2 异常处理 | |
| 297 | + socket.exceptionHandler(e -> { | |
| 298 | + log.error("[handleConnection][连接异常, remoteAddress={}]", socket.remoteAddress(), e); | |
| 299 | + socket.close(); | |
| 300 | + }); | |
| 301 | + } | |
| 302 | + | |
| 303 | + /** | |
| 304 | + * 刷新已连接设备的配置(定时调用) | |
| 305 | + */ | |
| 306 | + private synchronized void refreshConfig() { | |
| 307 | + try { | |
| 308 | + // 1. 只刷新已连接设备的配置 | |
| 309 | + Set<Long> connectedDeviceIds = connectionManager.getConnectedDeviceIds(); | |
| 310 | + if (CollUtil.isEmpty(connectedDeviceIds)) { | |
| 311 | + return; | |
| 312 | + } | |
| 313 | + List<IotModbusDeviceConfigRespDTO> configs = | |
| 314 | + configCacheService.refreshConnectedDeviceConfigList(connectedDeviceIds); | |
| 315 | + if (configs == null) { | |
| 316 | + log.warn("[refreshConfig][刷新配置失败,跳过本次刷新]"); | |
| 317 | + return; | |
| 318 | + } | |
| 319 | + log.debug("[refreshConfig][刷新了 {} 个已连接设备的配置]", configs.size()); | |
| 320 | + | |
| 321 | + // 2. 更新已连接设备的轮询任务 | |
| 322 | + for (IotModbusDeviceConfigRespDTO config : configs) { | |
| 323 | + try { | |
| 324 | + pollScheduler.updatePolling(config); | |
| 325 | + } catch (Exception e) { | |
| 326 | + log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); | |
| 327 | + } | |
| 328 | + } | |
| 329 | + } catch (Exception e) { | |
| 330 | + log.error("[refreshConfig][刷新配置失败]", e); | |
| 331 | + } | |
| 332 | + } | |
| 333 | + | |
| 334 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrame.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; | |
| 5 | +import lombok.Data; | |
| 6 | +import lombok.experimental.Accessors; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * IoT Modbus 统一帧数据模型(TCP/RTU 公用) | |
| 10 | + * | |
| 11 | + * @author 芋道源码 | |
| 12 | + */ | |
| 13 | +@Data | |
| 14 | +@Accessors(chain = true) | |
| 15 | +public class IotModbusFrame { | |
| 16 | + | |
| 17 | + /** | |
| 18 | + * 从站地址 | |
| 19 | + */ | |
| 20 | + private int slaveId; | |
| 21 | + /** | |
| 22 | + * 功能码 | |
| 23 | + */ | |
| 24 | + private int functionCode; | |
| 25 | + /** | |
| 26 | + * PDU 数据(不含 slaveId) | |
| 27 | + */ | |
| 28 | + private byte[] pdu; | |
| 29 | + /** | |
| 30 | + * 事务标识符 | |
| 31 | + * <p> | |
| 32 | + * 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值 | |
| 33 | + */ | |
| 34 | + private Integer transactionId; | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * 异常码 | |
| 38 | + * <p> | |
| 39 | + * 当功能码最高位为 1 时(异常响应),此字段存储异常码。 | |
| 40 | + * | |
| 41 | + * @see IotModbusCommonUtils#FC_EXCEPTION_MASK | |
| 42 | + */ | |
| 43 | + private Integer exceptionCode; | |
| 44 | + | |
| 45 | + /** | |
| 46 | + * 自定义功能码时的 JSON 字符串(用于 auth 认证等等) | |
| 47 | + */ | |
| 48 | + private String customData; | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * 是否异常响应(基于 exceptionCode 是否有值判断) | |
| 52 | + */ | |
| 53 | + public boolean isException() { | |
| 54 | + return exceptionCode != null; | |
| 55 | + } | |
| 56 | + | |
| 57 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameDecoder.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; | |
| 5 | +import io.vertx.core.Handler; | |
| 6 | +import io.vertx.core.buffer.Buffer; | |
| 7 | +import io.vertx.core.parsetools.RecordParser; | |
| 8 | +import lombok.RequiredArgsConstructor; | |
| 9 | +import lombok.extern.slf4j.Slf4j; | |
| 10 | + | |
| 11 | +import java.nio.ByteBuffer; | |
| 12 | +import java.nio.ByteOrder; | |
| 13 | +import java.nio.charset.StandardCharsets; | |
| 14 | +import java.util.function.BiConsumer; | |
| 15 | + | |
| 16 | +/** | |
| 17 | + * IoT Modbus 帧解码器:集成 TCP 拆包 + 帧格式探测 + 帧解码,一条龙完成从 TCP 字节流到 IotModbusFrame 的转换。 | |
| 18 | + * <p> | |
| 19 | + * 流程: | |
| 20 | + * 1. 首帧检测:读前 6 字节,判断 MODBUS_TCP(ProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU | |
| 21 | + * 2. 检测后切换到对应的拆包 Handler,并将首包 6 字节通过 handleFirstBytes() 交给新 Handler 处理 | |
| 22 | + * 3. 拆包完成后解码为 IotModbusFrame,通过回调返回 | |
| 23 | + * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动) | |
| 24 | + * - MODBUS_RTU:功能码驱动的状态机 | |
| 25 | + * | |
| 26 | + * @author 芋道源码 | |
| 27 | + */ | |
| 28 | +@RequiredArgsConstructor | |
| 29 | +@Slf4j | |
| 30 | +public class IotModbusFrameDecoder { | |
| 31 | + | |
| 32 | + private static final Boolean REQUEST_MODE_DEFAULT = false; | |
| 33 | + | |
| 34 | + /** | |
| 35 | + * 自定义功能码 | |
| 36 | + */ | |
| 37 | + private final int customFunctionCode; | |
| 38 | + | |
| 39 | + /** | |
| 40 | + * 创建带自动帧格式检测的 RecordParser(默认响应模式) | |
| 41 | + * | |
| 42 | + * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式) | |
| 43 | + * @return RecordParser 实例 | |
| 44 | + */ | |
| 45 | + public RecordParser createRecordParser(BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler) { | |
| 46 | + return createRecordParser(frameHandler, REQUEST_MODE_DEFAULT); | |
| 47 | + } | |
| 48 | + | |
| 49 | + /** | |
| 50 | + * 创建带自动帧格式检测的 RecordParser | |
| 51 | + * | |
| 52 | + * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式) | |
| 53 | + * @param requestMode 是否为请求模式(true:接收方收到的是 Modbus 请求帧,FC01-04 按固定 8 字节解析; | |
| 54 | + * false:接收方收到的是 Modbus 响应帧,FC01-04 按 byteCount 变长解析) | |
| 55 | + * @return RecordParser 实例 | |
| 56 | + */ | |
| 57 | + public RecordParser createRecordParser(BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler, | |
| 58 | + boolean requestMode) { | |
| 59 | + // 先创建一个 RecordParser:使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测 | |
| 60 | + RecordParser parser = RecordParser.newFixed(6); | |
| 61 | + parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler, requestMode)); | |
| 62 | + return parser; | |
| 63 | + } | |
| 64 | + | |
| 65 | + // ==================== 帧解码 ==================== | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * 解码响应帧(拆包后的完整帧 byte[]) | |
| 69 | + * | |
| 70 | + * @param data 完整帧字节数组 | |
| 71 | + * @param format 帧格式 | |
| 72 | + * @return 解码后的 IotModbusFrame | |
| 73 | + */ | |
| 74 | + private IotModbusFrame decodeResponse(byte[] data, IotModbusFrameFormatEnum format) { | |
| 75 | + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { | |
| 76 | + return decodeTcpResponse(data); | |
| 77 | + } else { | |
| 78 | + return decodeRtuResponse(data); | |
| 79 | + } | |
| 80 | + } | |
| 81 | + | |
| 82 | + /** | |
| 83 | + * 解码 MODBUS_TCP 响应 | |
| 84 | + * 格式:[TransactionId(2)] [ProtocolId(2)] [Length(2)] [UnitId(1)] [FC(1)] [Data...] | |
| 85 | + */ | |
| 86 | + private IotModbusFrame decodeTcpResponse(byte[] data) { | |
| 87 | + if (data.length < 8) { | |
| 88 | + log.warn("[decodeTcpResponse][数据长度不足: {}]", data.length); | |
| 89 | + return null; | |
| 90 | + } | |
| 91 | + ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN); | |
| 92 | + int transactionId = buf.getShort() & 0xFFFF; | |
| 93 | + buf.getShort(); // protocolId:固定 0x0000,Modbus 协议标识 | |
| 94 | + buf.getShort(); // length:后续字节数(UnitId + PDU),拆包阶段已使用 | |
| 95 | + int slaveId = buf.get() & 0xFF; | |
| 96 | + int functionCode = buf.get() & 0xFF; | |
| 97 | + // 提取 PDU 数据(从 functionCode 之后到末尾) | |
| 98 | + byte[] pdu = new byte[data.length - 8]; | |
| 99 | + System.arraycopy(data, 8, pdu, 0, pdu.length); | |
| 100 | + // 构建 IotModbusFrame | |
| 101 | + return buildFrame(slaveId, functionCode, pdu, transactionId); | |
| 102 | + } | |
| 103 | + | |
| 104 | + /** | |
| 105 | + * 解码 MODBUS_RTU 响应 | |
| 106 | + * 格式:[SlaveId(1)] [FC(1)] [Data...] [CRC(2)] | |
| 107 | + */ | |
| 108 | + private IotModbusFrame decodeRtuResponse(byte[] data) { | |
| 109 | + if (data.length < 4) { | |
| 110 | + log.warn("[decodeRtuResponse][数据长度不足: {}]", data.length); | |
| 111 | + return null; | |
| 112 | + } | |
| 113 | + // 校验 CRC | |
| 114 | + if (!IotModbusCommonUtils.verifyCrc16(data)) { | |
| 115 | + log.warn("[decodeRtuResponse][CRC 校验失败]"); | |
| 116 | + return null; | |
| 117 | + } | |
| 118 | + int slaveId = data[0] & 0xFF; | |
| 119 | + int functionCode = data[1] & 0xFF; | |
| 120 | + // PDU 数据(不含 slaveId、functionCode、CRC) | |
| 121 | + byte[] pdu = new byte[data.length - 4]; | |
| 122 | + System.arraycopy(data, 2, pdu, 0, pdu.length); | |
| 123 | + // 构建 IotModbusFrame | |
| 124 | + return buildFrame(slaveId, functionCode, pdu, null); | |
| 125 | + } | |
| 126 | + | |
| 127 | + /** | |
| 128 | + * 构建 IotModbusFrame | |
| 129 | + */ | |
| 130 | + private IotModbusFrame buildFrame(int slaveId, int functionCode, byte[] pdu, Integer transactionId) { | |
| 131 | + IotModbusFrame frame = new IotModbusFrame() | |
| 132 | + .setSlaveId(slaveId) | |
| 133 | + .setFunctionCode(functionCode) | |
| 134 | + .setPdu(pdu) | |
| 135 | + .setTransactionId(transactionId); | |
| 136 | + // 异常响应 | |
| 137 | + if (IotModbusCommonUtils.isExceptionResponse(functionCode)) { | |
| 138 | + frame.setFunctionCode(IotModbusCommonUtils.extractOriginalFunctionCode(functionCode)); | |
| 139 | + if (pdu.length >= 1) { | |
| 140 | + frame.setExceptionCode(pdu[0] & 0xFF); | |
| 141 | + } | |
| 142 | + return frame; | |
| 143 | + } | |
| 144 | + // 自定义功能码 | |
| 145 | + if (functionCode == customFunctionCode) { | |
| 146 | + // data 区格式:[byteCount(1)] [JSON data(N)] | |
| 147 | + if (pdu.length >= 1) { | |
| 148 | + int byteCount = pdu[0] & 0xFF; | |
| 149 | + if (pdu.length >= 1 + byteCount) { | |
| 150 | + frame.setCustomData(new String(pdu, 1, byteCount, StandardCharsets.UTF_8)); | |
| 151 | + } | |
| 152 | + } | |
| 153 | + } | |
| 154 | + return frame; | |
| 155 | + } | |
| 156 | + | |
| 157 | + // ==================== 拆包 Handler ==================== | |
| 158 | + | |
| 159 | + /** | |
| 160 | + * 帧格式检测阶段 Handler(仅处理首包,探测后切换到对应的拆包 Handler) | |
| 161 | + */ | |
| 162 | + @RequiredArgsConstructor | |
| 163 | + private class DetectPhaseHandler implements Handler<Buffer> { | |
| 164 | + | |
| 165 | + private final RecordParser parser; | |
| 166 | + private final int customFunctionCode; | |
| 167 | + private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler; | |
| 168 | + private final boolean requestMode; | |
| 169 | + | |
| 170 | + @Override | |
| 171 | + public void handle(Buffer buffer) { | |
| 172 | + // 检测帧格式:protocolId==0x0000 且 length 合法 → MODBUS_TCP,否则 → MODBUS_RTU | |
| 173 | + byte[] bytes = buffer.getBytes(); | |
| 174 | + int protocolId = ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); | |
| 175 | + int length = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF); | |
| 176 | + | |
| 177 | + // 分别处理 MODBUS_TCP、MODBUS_RTU 两种情况 | |
| 178 | + if (protocolId == 0x0000 && length >= 1 && length <= 253) { | |
| 179 | + // MODBUS_TCP:切换到 TCP 拆包 Handler | |
| 180 | + log.debug("[DetectPhaseHandler][检测到 MODBUS_TCP 帧格式]"); | |
| 181 | + TcpFrameHandler tcpHandler = new TcpFrameHandler(parser, frameHandler); | |
| 182 | + parser.handler(tcpHandler); | |
| 183 | + // 当前 bytes 就是 MBAP 的前 6 字节,直接交给 tcpHandler 处理 | |
| 184 | + tcpHandler.handleFirstBytes(bytes); | |
| 185 | + } else { | |
| 186 | + // MODBUS_RTU:切换到 RTU 拆包 Handler | |
| 187 | + log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]"); | |
| 188 | + RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, frameHandler, customFunctionCode, requestMode); | |
| 189 | + parser.handler(rtuHandler); | |
| 190 | + // 当前 bytes 包含前 6 字节(slaveId + FC + 部分数据),交给 rtuHandler 处理 | |
| 191 | + rtuHandler.handleFirstBytes(bytes); | |
| 192 | + } | |
| 193 | + } | |
| 194 | + } | |
| 195 | + | |
| 196 | + /** | |
| 197 | + * MODBUS_TCP 拆包 Handler(两阶段 RecordParser) | |
| 198 | + * <p> | |
| 199 | + * Phase 1: fixedSizeMode(6) → 读 MBAP 前 6 字节,提取 length | |
| 200 | + * Phase 2: fixedSizeMode(length) → 读 unitId + PDU | |
| 201 | + */ | |
| 202 | + @RequiredArgsConstructor | |
| 203 | + private class TcpFrameHandler implements Handler<Buffer> { | |
| 204 | + | |
| 205 | + private final RecordParser parser; | |
| 206 | + private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler; | |
| 207 | + | |
| 208 | + private byte[] mbapHeader; | |
| 209 | + private boolean waitingForBody = false; | |
| 210 | + | |
| 211 | + /** | |
| 212 | + * 处理探测阶段传来的首帧 6 字节(即 MBAP 头) | |
| 213 | + * | |
| 214 | + * @param bytes 探测阶段消费的 6 字节 | |
| 215 | + */ | |
| 216 | + void handleFirstBytes(byte[] bytes) { | |
| 217 | + int length = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF); | |
| 218 | + this.mbapHeader = bytes; | |
| 219 | + this.waitingForBody = true; | |
| 220 | + parser.fixedSizeMode(length); | |
| 221 | + } | |
| 222 | + | |
| 223 | + @Override | |
| 224 | + public void handle(Buffer buffer) { | |
| 225 | + if (waitingForBody) { | |
| 226 | + // Phase 2: 收到 body(unitId + PDU) | |
| 227 | + byte[] body = buffer.getBytes(); | |
| 228 | + // 拼接完整帧:MBAP(6) + body | |
| 229 | + byte[] fullFrame = new byte[mbapHeader.length + body.length]; | |
| 230 | + System.arraycopy(mbapHeader, 0, fullFrame, 0, mbapHeader.length); | |
| 231 | + System.arraycopy(body, 0, fullFrame, mbapHeader.length, body.length); | |
| 232 | + // 解码并回调 | |
| 233 | + IotModbusFrame frame = decodeResponse(fullFrame, IotModbusFrameFormatEnum.MODBUS_TCP); | |
| 234 | + if (frame != null) { | |
| 235 | + frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_TCP); | |
| 236 | + } | |
| 237 | + // 切回 Phase 1 | |
| 238 | + waitingForBody = false; | |
| 239 | + mbapHeader = null; | |
| 240 | + parser.fixedSizeMode(6); | |
| 241 | + } else { | |
| 242 | + // Phase 1: 收到 MBAP 头 6 字节 | |
| 243 | + byte[] header = buffer.getBytes(); | |
| 244 | + int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF); | |
| 245 | + if (length < 1 || length > 253) { | |
| 246 | + log.warn("[TcpFrameHandler][MBAP Length 异常: {}]", length); | |
| 247 | + parser.fixedSizeMode(6); | |
| 248 | + return; | |
| 249 | + } | |
| 250 | + this.mbapHeader = header; | |
| 251 | + this.waitingForBody = true; | |
| 252 | + parser.fixedSizeMode(length); | |
| 253 | + } | |
| 254 | + } | |
| 255 | + } | |
| 256 | + | |
| 257 | + /** | |
| 258 | + * MODBUS_RTU 拆包 Handler(功能码驱动的状态机) | |
| 259 | + * <p> | |
| 260 | + * 状态机流程: | |
| 261 | + * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode | |
| 262 | + * Phase 2: 根据 functionCode 确定剩余长度: | |
| 263 | + * - 异常响应 (FC & EXCEPTION_MASK):fixedSizeMode(3) → exceptionCode(1) + CRC(2) | |
| 264 | + * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2) | |
| 265 | + * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2) | |
| 266 | + * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2) | |
| 267 | + * <p> | |
| 268 | + * 请求模式(requestMode=true)时,FC01-04 按固定 8 字节解析(与写响应相同路径), | |
| 269 | + * 因为读请求格式为 [SlaveId(1)][FC(1)][StartAddr(2)][Quantity(2)][CRC(2)] | |
| 270 | + */ | |
| 271 | + @RequiredArgsConstructor | |
| 272 | + private class RtuFrameHandler implements Handler<Buffer> { | |
| 273 | + | |
| 274 | + private static final int STATE_HEADER = 0; | |
| 275 | + private static final int STATE_EXCEPTION_BODY = 1; | |
| 276 | + private static final int STATE_READ_BYTE_COUNT = 2; | |
| 277 | + private static final int STATE_READ_DATA = 3; | |
| 278 | + private static final int STATE_WRITE_BODY = 4; | |
| 279 | + | |
| 280 | + private final RecordParser parser; | |
| 281 | + private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler; | |
| 282 | + private final int customFunctionCode; | |
| 283 | + /** | |
| 284 | + * 请求模式: | |
| 285 | + * - true 表示接收方收到的是 Modbus 请求帧(如设备端收到网关下发的读请求),FC01-04 按固定 8 字节帧解析 | |
| 286 | + * - false 表示接收方收到的是 Modbus 响应帧,FC01-04 按 byteCount 变长解析 | |
| 287 | + */ | |
| 288 | + private final boolean requestMode; | |
| 289 | + | |
| 290 | + private int state = STATE_HEADER; | |
| 291 | + private byte slaveId; | |
| 292 | + private byte functionCode; | |
| 293 | + private byte byteCount; | |
| 294 | + private Buffer pendingData; | |
| 295 | + private int expectedDataLen; | |
| 296 | + | |
| 297 | + /** | |
| 298 | + * 处理探测阶段传来的首帧 6 字节 | |
| 299 | + * <p> | |
| 300 | + * 由于 RTU 首帧被探测阶段消费了 6 字节,这里需要从中提取 slaveId + FC 并根据 FC 处理剩余数据 | |
| 301 | + * | |
| 302 | + * @param bytes 探测阶段消费的 6 字节:[slaveId][FC][...4 bytes...] | |
| 303 | + */ | |
| 304 | + void handleFirstBytes(byte[] bytes) { | |
| 305 | + this.slaveId = bytes[0]; | |
| 306 | + this.functionCode = bytes[1]; | |
| 307 | + int fc = functionCode & 0xFF; | |
| 308 | + if (IotModbusCommonUtils.isExceptionResponse(fc)) { | |
| 309 | + // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节 | |
| 310 | + // 已有 6 字节(多 1 字节),取前 5 字节组装 | |
| 311 | + Buffer frame = Buffer.buffer(5); | |
| 312 | + frame.appendByte(slaveId); | |
| 313 | + frame.appendByte(functionCode); | |
| 314 | + frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC | |
| 315 | + emitFrame(frame); | |
| 316 | + resetToHeader(); | |
| 317 | + } else if (IotModbusCommonUtils.isReadResponse(fc) && requestMode) { | |
| 318 | + // 请求模式下的读请求:固定 8 字节 [SlaveId(1)][FC(1)][StartAddr(2)][Quantity(2)][CRC(2)] | |
| 319 | + // 已有 6 字节,还需 2 字节(CRC) | |
| 320 | + state = STATE_WRITE_BODY; | |
| 321 | + this.pendingData = Buffer.buffer(); | |
| 322 | + this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节(StartAddr + Quantity) | |
| 323 | + parser.fixedSizeMode(2); // 还需 2 字节(CRC) | |
| 324 | + } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) { | |
| 325 | + // 读响应或自定义 FC:bytes[2] = byteCount | |
| 326 | + this.byteCount = bytes[2]; | |
| 327 | + int bc = byteCount & 0xFF; | |
| 328 | + // 已有数据:bytes[3..5] = 3 字节 | |
| 329 | + // 还需:byteCount + CRC(2) - 3 字节已有 | |
| 330 | + int remaining = bc + 2 - 3; | |
| 331 | + if (remaining <= 0) { | |
| 332 | + // 数据已足够,组装完整帧 | |
| 333 | + int totalLen = 2 + 1 + bc + 2; // slaveId + FC + byteCount + data + CRC | |
| 334 | + Buffer frame = Buffer.buffer(totalLen); | |
| 335 | + frame.appendByte(slaveId); | |
| 336 | + frame.appendByte(functionCode); | |
| 337 | + frame.appendByte(byteCount); | |
| 338 | + frame.appendBytes(bytes, 3, bc + 2); // data + CRC | |
| 339 | + emitFrame(frame); | |
| 340 | + resetToHeader(); | |
| 341 | + } else { | |
| 342 | + // 需要继续读 | |
| 343 | + state = STATE_READ_DATA; | |
| 344 | + this.pendingData = Buffer.buffer(); | |
| 345 | + this.pendingData.appendBytes(bytes, 3, 3); // 暂存已有的 3 字节 | |
| 346 | + this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC | |
| 347 | + parser.fixedSizeMode(remaining); | |
| 348 | + } | |
| 349 | + } else if (IotModbusCommonUtils.isWriteResponse(fc)) { | |
| 350 | + // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节 | |
| 351 | + // 已有 6 字节,还需 2 字节 | |
| 352 | + state = STATE_WRITE_BODY; | |
| 353 | + this.pendingData = Buffer.buffer(); | |
| 354 | + this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节 | |
| 355 | + parser.fixedSizeMode(2); // 还需 2 字节(CRC) | |
| 356 | + } else { | |
| 357 | + log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc)); | |
| 358 | + resetToHeader(); | |
| 359 | + } | |
| 360 | + } | |
| 361 | + | |
| 362 | + @Override | |
| 363 | + public void handle(Buffer buffer) { | |
| 364 | + switch (state) { | |
| 365 | + case STATE_HEADER: | |
| 366 | + handleHeader(buffer); | |
| 367 | + break; | |
| 368 | + case STATE_EXCEPTION_BODY: | |
| 369 | + handleExceptionBody(buffer); | |
| 370 | + break; | |
| 371 | + case STATE_READ_BYTE_COUNT: | |
| 372 | + handleReadByteCount(buffer); | |
| 373 | + break; | |
| 374 | + case STATE_READ_DATA: | |
| 375 | + handleReadData(buffer); | |
| 376 | + break; | |
| 377 | + case STATE_WRITE_BODY: | |
| 378 | + handleWriteBody(buffer); | |
| 379 | + break; | |
| 380 | + default: | |
| 381 | + resetToHeader(); | |
| 382 | + } | |
| 383 | + } | |
| 384 | + | |
| 385 | + private void handleHeader(Buffer buffer) { | |
| 386 | + byte[] header = buffer.getBytes(); | |
| 387 | + this.slaveId = header[0]; | |
| 388 | + this.functionCode = header[1]; | |
| 389 | + int fc = functionCode & 0xFF; | |
| 390 | + if (IotModbusCommonUtils.isExceptionResponse(fc)) { | |
| 391 | + // 异常响应 | |
| 392 | + state = STATE_EXCEPTION_BODY; | |
| 393 | + parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2) | |
| 394 | + } else if (IotModbusCommonUtils.isReadResponse(fc) && requestMode) { | |
| 395 | + // 请求模式下的读请求:固定 8 字节,已读 2 字节(slaveId + FC),还需 6 字节 | |
| 396 | + state = STATE_WRITE_BODY; | |
| 397 | + pendingData = Buffer.buffer(); | |
| 398 | + parser.fixedSizeMode(6); // StartAddr(2) + Quantity(2) + CRC(2) | |
| 399 | + } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) { | |
| 400 | + // 读响应或自定义 FC | |
| 401 | + state = STATE_READ_BYTE_COUNT; | |
| 402 | + parser.fixedSizeMode(1); // byteCount | |
| 403 | + } else if (IotModbusCommonUtils.isWriteResponse(fc)) { | |
| 404 | + // 写响应 | |
| 405 | + state = STATE_WRITE_BODY; | |
| 406 | + pendingData = Buffer.buffer(); | |
| 407 | + parser.fixedSizeMode(6); // addr(2) + value(2) + CRC(2) | |
| 408 | + } else { | |
| 409 | + log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc)); | |
| 410 | + resetToHeader(); | |
| 411 | + } | |
| 412 | + } | |
| 413 | + | |
| 414 | + private void handleExceptionBody(Buffer buffer) { | |
| 415 | + // buffer = exceptionCode(1) + CRC(2) | |
| 416 | + Buffer frame = Buffer.buffer(); | |
| 417 | + frame.appendByte(slaveId); | |
| 418 | + frame.appendByte(functionCode); | |
| 419 | + frame.appendBuffer(buffer); | |
| 420 | + emitFrame(frame); | |
| 421 | + resetToHeader(); | |
| 422 | + } | |
| 423 | + | |
| 424 | + private void handleReadByteCount(Buffer buffer) { | |
| 425 | + this.byteCount = buffer.getByte(0); | |
| 426 | + int bc = byteCount & 0xFF; | |
| 427 | + state = STATE_READ_DATA; | |
| 428 | + pendingData = Buffer.buffer(); | |
| 429 | + expectedDataLen = bc + 2; // data(bc) + CRC(2) | |
| 430 | + parser.fixedSizeMode(expectedDataLen); | |
| 431 | + } | |
| 432 | + | |
| 433 | + private void handleReadData(Buffer buffer) { | |
| 434 | + pendingData.appendBuffer(buffer); | |
| 435 | + if (pendingData.length() >= expectedDataLen) { | |
| 436 | + // 组装完整帧 | |
| 437 | + Buffer frame = Buffer.buffer(); | |
| 438 | + frame.appendByte(slaveId); | |
| 439 | + frame.appendByte(functionCode); | |
| 440 | + frame.appendByte(byteCount); | |
| 441 | + frame.appendBuffer(pendingData); | |
| 442 | + emitFrame(frame); | |
| 443 | + resetToHeader(); | |
| 444 | + } | |
| 445 | + // 否则继续等待(不应该发生,因为我们精确设置了 fixedSizeMode) | |
| 446 | + } | |
| 447 | + | |
| 448 | + private void handleWriteBody(Buffer buffer) { | |
| 449 | + pendingData.appendBuffer(buffer); | |
| 450 | + // 完整帧 | |
| 451 | + Buffer frame = Buffer.buffer(); | |
| 452 | + frame.appendByte(slaveId); | |
| 453 | + frame.appendByte(functionCode); | |
| 454 | + frame.appendBuffer(pendingData); | |
| 455 | + emitFrame(frame); | |
| 456 | + resetToHeader(); | |
| 457 | + } | |
| 458 | + | |
| 459 | + /** | |
| 460 | + * 发射完整帧:解码并回调 | |
| 461 | + */ | |
| 462 | + private void emitFrame(Buffer frameBuffer) { | |
| 463 | + IotModbusFrame frame = decodeResponse(frameBuffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU); | |
| 464 | + if (frame != null) { | |
| 465 | + frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_RTU); | |
| 466 | + } | |
| 467 | + } | |
| 468 | + | |
| 469 | + private void resetToHeader() { | |
| 470 | + state = STATE_HEADER; | |
| 471 | + pendingData = null; | |
| 472 | + parser.fixedSizeMode(2); // slaveId + FC | |
| 473 | + } | |
| 474 | + | |
| 475 | + } | |
| 476 | + | |
| 477 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameEncoder.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; | |
| 5 | +import lombok.RequiredArgsConstructor; | |
| 6 | +import lombok.extern.slf4j.Slf4j; | |
| 7 | + | |
| 8 | +import java.nio.charset.StandardCharsets; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * IoT Modbus 帧编码器:负责将 Modbus 请求/响应编码为字节数组,支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式。 | |
| 12 | + * | |
| 13 | + * @author 芋道源码 | |
| 14 | + */ | |
| 15 | +@RequiredArgsConstructor | |
| 16 | +@Slf4j | |
| 17 | +public class IotModbusFrameEncoder { | |
| 18 | + | |
| 19 | + private final int customFunctionCode; | |
| 20 | + | |
| 21 | + // ==================== 编码 ==================== | |
| 22 | + | |
| 23 | + /** | |
| 24 | + * 编码读请求 | |
| 25 | + * | |
| 26 | + * @param slaveId 从站地址 | |
| 27 | + * @param functionCode 功能码 | |
| 28 | + * @param startAddress 起始寄存器地址 | |
| 29 | + * @param quantity 寄存器数量 | |
| 30 | + * @param format 帧格式 | |
| 31 | + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) | |
| 32 | + * @return 编码后的字节数组 | |
| 33 | + */ | |
| 34 | + public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity, | |
| 35 | + IotModbusFrameFormatEnum format, Integer transactionId) { | |
| 36 | + // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)] | |
| 37 | + byte[] pdu = new byte[5]; | |
| 38 | + pdu[0] = (byte) functionCode; | |
| 39 | + pdu[1] = (byte) ((startAddress >> 8) & 0xFF); | |
| 40 | + pdu[2] = (byte) (startAddress & 0xFF); | |
| 41 | + pdu[3] = (byte) ((quantity >> 8) & 0xFF); | |
| 42 | + pdu[4] = (byte) (quantity & 0xFF); | |
| 43 | + return wrapFrame(slaveId, pdu, format, transactionId); | |
| 44 | + } | |
| 45 | + | |
| 46 | + /** | |
| 47 | + * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05) | |
| 48 | + * | |
| 49 | + * @param slaveId 从站地址 | |
| 50 | + * @param functionCode 功能码 | |
| 51 | + * @param address 寄存器地址 | |
| 52 | + * @param value 值 | |
| 53 | + * @param format 帧格式 | |
| 54 | + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) | |
| 55 | + * @return 编码后的字节数组 | |
| 56 | + */ | |
| 57 | + public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value, | |
| 58 | + IotModbusFrameFormatEnum format, Integer transactionId) { | |
| 59 | + // FC05 单写线圈:Modbus 标准要求 value 为 0xFF00(ON)或 0x0000(OFF) | |
| 60 | + if (functionCode == IotModbusCommonUtils.FC_WRITE_SINGLE_COIL) { | |
| 61 | + value = (value != 0) ? 0xFF00 : 0x0000; | |
| 62 | + } | |
| 63 | + // PDU: [FC(1)] [Address(2)] [Value(2)] | |
| 64 | + byte[] pdu = new byte[5]; | |
| 65 | + pdu[0] = (byte) functionCode; | |
| 66 | + pdu[1] = (byte) ((address >> 8) & 0xFF); | |
| 67 | + pdu[2] = (byte) (address & 0xFF); | |
| 68 | + pdu[3] = (byte) ((value >> 8) & 0xFF); | |
| 69 | + pdu[4] = (byte) (value & 0xFF); | |
| 70 | + return wrapFrame(slaveId, pdu, format, transactionId); | |
| 71 | + } | |
| 72 | + | |
| 73 | + /** | |
| 74 | + * 编码写多个寄存器请求(FC16) | |
| 75 | + * | |
| 76 | + * @param slaveId 从站地址 | |
| 77 | + * @param address 起始地址 | |
| 78 | + * @param values 值数组 | |
| 79 | + * @param format 帧格式 | |
| 80 | + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) | |
| 81 | + * @return 编码后的字节数组 | |
| 82 | + */ | |
| 83 | + public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values, | |
| 84 | + IotModbusFrameFormatEnum format, Integer transactionId) { | |
| 85 | + // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)] | |
| 86 | + int quantity = values.length; | |
| 87 | + int byteCount = quantity * 2; | |
| 88 | + byte[] pdu = new byte[6 + byteCount]; | |
| 89 | + pdu[0] = (byte) 16; // FC16 | |
| 90 | + pdu[1] = (byte) ((address >> 8) & 0xFF); | |
| 91 | + pdu[2] = (byte) (address & 0xFF); | |
| 92 | + pdu[3] = (byte) ((quantity >> 8) & 0xFF); | |
| 93 | + pdu[4] = (byte) (quantity & 0xFF); | |
| 94 | + pdu[5] = (byte) byteCount; | |
| 95 | + for (int i = 0; i < quantity; i++) { | |
| 96 | + pdu[6 + i * 2] = (byte) ((values[i] >> 8) & 0xFF); | |
| 97 | + pdu[6 + i * 2 + 1] = (byte) (values[i] & 0xFF); | |
| 98 | + } | |
| 99 | + return wrapFrame(slaveId, pdu, format, transactionId); | |
| 100 | + } | |
| 101 | + | |
| 102 | + /** | |
| 103 | + * 编码写多个线圈请求(FC15) | |
| 104 | + * <p> | |
| 105 | + * 按 Modbus FC15 标准,线圈值按 bit 打包(每个 byte 包含 8 个线圈状态)。 | |
| 106 | + * | |
| 107 | + * @param slaveId 从站地址 | |
| 108 | + * @param address 起始地址 | |
| 109 | + * @param values 线圈值数组(int[],非0 表示 ON,0 表示 OFF) | |
| 110 | + * @param format 帧格式 | |
| 111 | + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) | |
| 112 | + * @return 编码后的字节数组 | |
| 113 | + */ | |
| 114 | + public byte[] encodeWriteMultipleCoilsRequest(int slaveId, int address, int[] values, | |
| 115 | + IotModbusFrameFormatEnum format, Integer transactionId) { | |
| 116 | + // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [CoilValues(N)] | |
| 117 | + int quantity = values.length; | |
| 118 | + int byteCount = (quantity + 7) / 8; // 向上取整 | |
| 119 | + byte[] pdu = new byte[6 + byteCount]; | |
| 120 | + pdu[0] = (byte) IotModbusCommonUtils.FC_WRITE_MULTIPLE_COILS; // FC15 | |
| 121 | + pdu[1] = (byte) ((address >> 8) & 0xFF); | |
| 122 | + pdu[2] = (byte) (address & 0xFF); | |
| 123 | + pdu[3] = (byte) ((quantity >> 8) & 0xFF); | |
| 124 | + pdu[4] = (byte) (quantity & 0xFF); | |
| 125 | + pdu[5] = (byte) byteCount; | |
| 126 | + // 按 bit 打包:每个 byte 的 bit0 对应最低地址的线圈 | |
| 127 | + for (int i = 0; i < quantity; i++) { | |
| 128 | + if (values[i] != 0) { | |
| 129 | + pdu[6 + i / 8] |= (byte) (1 << (i % 8)); | |
| 130 | + } | |
| 131 | + } | |
| 132 | + return wrapFrame(slaveId, pdu, format, transactionId); | |
| 133 | + } | |
| 134 | + | |
| 135 | + /** | |
| 136 | + * 编码自定义功能码帧(认证响应等) | |
| 137 | + * | |
| 138 | + * @param slaveId 从站地址 | |
| 139 | + * @param jsonData JSON 数据 | |
| 140 | + * @param format 帧格式 | |
| 141 | + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) | |
| 142 | + * @return 编码后的字节数组 | |
| 143 | + */ | |
| 144 | + public byte[] encodeCustomFrame(int slaveId, String jsonData, | |
| 145 | + IotModbusFrameFormatEnum format, Integer transactionId) { | |
| 146 | + byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8); | |
| 147 | + // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)] | |
| 148 | + byte[] pdu = new byte[2 + jsonBytes.length]; | |
| 149 | + pdu[0] = (byte) customFunctionCode; | |
| 150 | + pdu[1] = (byte) jsonBytes.length; | |
| 151 | + System.arraycopy(jsonBytes, 0, pdu, 2, jsonBytes.length); | |
| 152 | + return wrapFrame(slaveId, pdu, format, transactionId); | |
| 153 | + } | |
| 154 | + | |
| 155 | + // ==================== 帧封装 ==================== | |
| 156 | + | |
| 157 | + /** | |
| 158 | + * 将 PDU 封装为完整帧 | |
| 159 | + * | |
| 160 | + * @param slaveId 从站地址 | |
| 161 | + * @param pdu PDU 数据(含 functionCode) | |
| 162 | + * @param format 帧格式 | |
| 163 | + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式可为 null) | |
| 164 | + * @return 完整帧字节数组 | |
| 165 | + */ | |
| 166 | + private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, Integer transactionId) { | |
| 167 | + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { | |
| 168 | + return wrapTcpFrame(slaveId, pdu, transactionId != null ? transactionId : 0); | |
| 169 | + } else { | |
| 170 | + return wrapRtuFrame(slaveId, pdu); | |
| 171 | + } | |
| 172 | + } | |
| 173 | + | |
| 174 | + /** | |
| 175 | + * 封装 MODBUS_TCP 帧 | |
| 176 | + * [TransactionId(2)] [ProtocolId(2,=0x0000)] [Length(2)] [UnitId(1)] [PDU...] | |
| 177 | + */ | |
| 178 | + private byte[] wrapTcpFrame(int slaveId, byte[] pdu, int transactionId) { | |
| 179 | + int length = 1 + pdu.length; // UnitId + PDU | |
| 180 | + byte[] frame = new byte[6 + length]; // MBAP(6) + UnitId(1) + PDU | |
| 181 | + // MBAP Header | |
| 182 | + frame[0] = (byte) ((transactionId >> 8) & 0xFF); | |
| 183 | + frame[1] = (byte) (transactionId & 0xFF); | |
| 184 | + frame[2] = 0; // Protocol ID high | |
| 185 | + frame[3] = 0; // Protocol ID low | |
| 186 | + frame[4] = (byte) ((length >> 8) & 0xFF); | |
| 187 | + frame[5] = (byte) (length & 0xFF); | |
| 188 | + // Unit ID | |
| 189 | + frame[6] = (byte) slaveId; | |
| 190 | + // PDU | |
| 191 | + System.arraycopy(pdu, 0, frame, 7, pdu.length); | |
| 192 | + return frame; | |
| 193 | + } | |
| 194 | + | |
| 195 | + /** | |
| 196 | + * 封装 MODBUS_RTU 帧 | |
| 197 | + * [SlaveId(1)] [PDU...] [CRC(2)] | |
| 198 | + */ | |
| 199 | + private byte[] wrapRtuFrame(int slaveId, byte[] pdu) { | |
| 200 | + byte[] frame = new byte[1 + pdu.length + 2]; // SlaveId + PDU + CRC | |
| 201 | + frame[0] = (byte) slaveId; | |
| 202 | + System.arraycopy(pdu, 0, frame, 1, pdu.length); | |
| 203 | + // 计算并追加 CRC16 | |
| 204 | + int crc = IotModbusCommonUtils.calculateCrc16(frame, frame.length - 2); | |
| 205 | + frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low | |
| 206 | + frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High | |
| 207 | + return frame; | |
| 208 | + } | |
| 209 | + | |
| 210 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.core.util.ObjUtil; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService; | |
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager; | |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo; | |
| 15 | +import lombok.extern.slf4j.Slf4j; | |
| 16 | + | |
| 17 | +import java.util.Map; | |
| 18 | +import java.util.concurrent.atomic.AtomicInteger; | |
| 19 | + | |
| 20 | +/** | |
| 21 | + * IoT Modbus TCP Server 下行消息处理器 | |
| 22 | + * <p> | |
| 23 | + * 负责: | |
| 24 | + * 1. 处理下行消息(如属性设置 thing.service.property.set) | |
| 25 | + * 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备 | |
| 26 | + * | |
| 27 | + * @author 芋道源码 | |
| 28 | + */ | |
| 29 | +@Slf4j | |
| 30 | +public class IotModbusTcpServerDownstreamHandler { | |
| 31 | + | |
| 32 | + private final IotModbusTcpServerConnectionManager connectionManager; | |
| 33 | + private final IotModbusTcpServerConfigCacheService configCacheService; | |
| 34 | + private final IotModbusFrameEncoder frameEncoder; | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * TCP 事务 ID 自增器(与 PollScheduler 共享) | |
| 38 | + */ | |
| 39 | + private final AtomicInteger transactionIdCounter; | |
| 40 | + | |
| 41 | + public IotModbusTcpServerDownstreamHandler(IotModbusTcpServerConnectionManager connectionManager, | |
| 42 | + IotModbusTcpServerConfigCacheService configCacheService, | |
| 43 | + IotModbusFrameEncoder frameEncoder, | |
| 44 | + AtomicInteger transactionIdCounter) { | |
| 45 | + this.connectionManager = connectionManager; | |
| 46 | + this.configCacheService = configCacheService; | |
| 47 | + this.frameEncoder = frameEncoder; | |
| 48 | + this.transactionIdCounter = transactionIdCounter; | |
| 49 | + } | |
| 50 | + | |
| 51 | + /** | |
| 52 | + * 处理下行消息 | |
| 53 | + */ | |
| 54 | + @SuppressWarnings({"unchecked", "DuplicatedCode"}) | |
| 55 | + public void handle(IotDeviceMessage message) { | |
| 56 | + // 1.1 检查是否是属性设置消息 | |
| 57 | + if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) { | |
| 58 | + return; | |
| 59 | + } | |
| 60 | + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { | |
| 61 | + log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); | |
| 62 | + return; | |
| 63 | + } | |
| 64 | + // 1.2 获取设备配置 | |
| 65 | + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId()); | |
| 66 | + if (config == null) { | |
| 67 | + log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId()); | |
| 68 | + return; | |
| 69 | + } | |
| 70 | + // 1.3 获取连接信息 | |
| 71 | + ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId()); | |
| 72 | + if (connInfo == null) { | |
| 73 | + log.warn("[handle][设备 {} 没有连接]", message.getDeviceId()); | |
| 74 | + return; | |
| 75 | + } | |
| 76 | + | |
| 77 | + // 2. 解析属性值并写入 | |
| 78 | + Object params = message.getParams(); | |
| 79 | + if (!(params instanceof Map)) { | |
| 80 | + log.warn("[handle][params 不是 Map 类型: {}]", params); | |
| 81 | + return; | |
| 82 | + } | |
| 83 | + Map<String, Object> propertyMap = (Map<String, Object>) params; | |
| 84 | + for (Map.Entry<String, Object> entry : propertyMap.entrySet()) { | |
| 85 | + String identifier = entry.getKey(); | |
| 86 | + Object value = entry.getValue(); | |
| 87 | + // 2.1 查找对应的点位配置 | |
| 88 | + IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier); | |
| 89 | + if (point == null) { | |
| 90 | + log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); | |
| 91 | + continue; | |
| 92 | + } | |
| 93 | + // 2.2 检查是否支持写操作 | |
| 94 | + if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) { | |
| 95 | + log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); | |
| 96 | + continue; | |
| 97 | + } | |
| 98 | + | |
| 99 | + // 2.3 执行写入 | |
| 100 | + writeProperty(config.getDeviceId(), connInfo, point, value); | |
| 101 | + } | |
| 102 | + } | |
| 103 | + | |
| 104 | + /** | |
| 105 | + * 写入属性值 | |
| 106 | + */ | |
| 107 | + private void writeProperty(Long deviceId, ConnectionInfo connInfo, | |
| 108 | + IotModbusPointRespDTO point, Object value) { | |
| 109 | + // 1.1 转换属性值为原始值 | |
| 110 | + int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point); | |
| 111 | + | |
| 112 | + // 1.2 确定帧格式和事务 ID | |
| 113 | + IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); | |
| 114 | + Assert.notNull(frameFormat, "连接帧格式不能为空"); | |
| 115 | + Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP | |
| 116 | + ? (transactionIdCounter.incrementAndGet() & 0xFFFF) | |
| 117 | + : null; | |
| 118 | + int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; | |
| 119 | + // 1.3 编码写请求 | |
| 120 | + byte[] data; | |
| 121 | + int readFunctionCode = point.getFunctionCode(); | |
| 122 | + Integer writeSingleCode = IotModbusCommonUtils.getWriteSingleFunctionCode(readFunctionCode); | |
| 123 | + Integer writeMultipleCode = IotModbusCommonUtils.getWriteMultipleFunctionCode(readFunctionCode); | |
| 124 | + if (rawValues.length == 1 && writeSingleCode != null) { | |
| 125 | + // 单个值:使用单写功能码(FC05/FC06) | |
| 126 | + data = frameEncoder.encodeWriteSingleRequest(slaveId, writeSingleCode, | |
| 127 | + point.getRegisterAddress(), rawValues[0], frameFormat, transactionId); | |
| 128 | + } else if (writeMultipleCode != null) { | |
| 129 | + // 多个值:使用多写功能码(FC15/FC16) | |
| 130 | + if (writeMultipleCode == IotModbusCommonUtils.FC_WRITE_MULTIPLE_COILS) { | |
| 131 | + data = frameEncoder.encodeWriteMultipleCoilsRequest(slaveId, | |
| 132 | + point.getRegisterAddress(), rawValues, frameFormat, transactionId); | |
| 133 | + } else { | |
| 134 | + data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId, | |
| 135 | + point.getRegisterAddress(), rawValues, frameFormat, transactionId); | |
| 136 | + } | |
| 137 | + } else { | |
| 138 | + log.warn("[writeProperty][点位 {} 不支持写操作, 功能码={}]", point.getIdentifier(), readFunctionCode); | |
| 139 | + return; | |
| 140 | + } | |
| 141 | + | |
| 142 | + // 2. 发送消息 | |
| 143 | + connectionManager.sendToDevice(deviceId, data).onSuccess(v -> | |
| 144 | + log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", | |
| 145 | + deviceId, point.getIdentifier(), value) | |
| 146 | + ).onFailure(e -> | |
| 147 | + log.error("[writeProperty][写入失败, deviceId={}, identifier={}, value={}]", | |
| 148 | + deviceId, point.getIdentifier(), value, e) | |
| 149 | + ); | |
| 150 | + } | |
| 151 | + | |
| 152 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamSubscriber.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol; | |
| 7 | +import lombok.extern.slf4j.Slf4j; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * IoT Modbus TCP Server 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Slf4j | |
| 15 | +public class IotModbusTcpServerDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { | |
| 16 | + | |
| 17 | + private final IotModbusTcpServerDownstreamHandler downstreamHandler; | |
| 18 | + | |
| 19 | + public IotModbusTcpServerDownstreamSubscriber(IotModbusTcpServerProtocol protocol, | |
| 20 | + IotModbusTcpServerDownstreamHandler downstreamHandler, | |
| 21 | + IotMessageBus messageBus) { | |
| 22 | + super(protocol, messageBus); | |
| 23 | + this.downstreamHandler = downstreamHandler; | |
| 24 | + } | |
| 25 | + | |
| 26 | + @Override | |
| 27 | + protected void handleMessage(IotDeviceMessage message) { | |
| 28 | + downstreamHandler.handle(message); | |
| 29 | + } | |
| 30 | + | |
| 31 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/upstream/IotModbusTcpServerUpstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.upstream; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.core.map.MapUtil; | |
| 5 | +import cn.hutool.core.util.BooleanUtil; | |
| 6 | +import cn.hutool.core.util.ObjUtil; | |
| 7 | +import cn.hutool.core.util.StrUtil; | |
| 8 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | |
| 9 | +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | |
| 10 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 11 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 12 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 13 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 14 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | |
| 15 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 16 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | |
| 17 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 18 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | |
| 19 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 20 | +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | |
| 21 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 22 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; | |
| 23 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; | |
| 24 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; | |
| 25 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService; | |
| 26 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager; | |
| 27 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo; | |
| 28 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager; | |
| 29 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager.PendingRequest; | |
| 30 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPollScheduler; | |
| 31 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | |
| 32 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | |
| 33 | +import io.vertx.core.net.NetSocket; | |
| 34 | +import lombok.extern.slf4j.Slf4j; | |
| 35 | + | |
| 36 | +import java.util.Map; | |
| 37 | + | |
| 38 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; | |
| 39 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | |
| 40 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; | |
| 41 | + | |
| 42 | +/** | |
| 43 | + * IoT Modbus TCP Server 上行数据处理器 | |
| 44 | + * <p> | |
| 45 | + * 处理: | |
| 46 | + * 1. 自定义 FC 认证 | |
| 47 | + * 2. 轮询响应 → 点位翻译 → thing.property.post | |
| 48 | + * | |
| 49 | + * @author 芋道源码 | |
| 50 | + */ | |
| 51 | +@Slf4j | |
| 52 | +public class IotModbusTcpServerUpstreamHandler { | |
| 53 | + | |
| 54 | + private static final String METHOD_AUTH = "auth"; | |
| 55 | + | |
| 56 | + private final IotDeviceCommonApi deviceApi; | |
| 57 | + private final IotDeviceMessageService messageService; | |
| 58 | + private final IotModbusFrameEncoder frameEncoder; | |
| 59 | + private final IotModbusTcpServerConnectionManager connectionManager; | |
| 60 | + private final IotModbusTcpServerConfigCacheService configCacheService; | |
| 61 | + private final IotModbusTcpServerPendingRequestManager pendingRequestManager; | |
| 62 | + private final IotModbusTcpServerPollScheduler pollScheduler; | |
| 63 | + private final IotDeviceService deviceService; | |
| 64 | + | |
| 65 | + private final String serverId; | |
| 66 | + | |
| 67 | + public IotModbusTcpServerUpstreamHandler(IotDeviceCommonApi deviceApi, | |
| 68 | + IotDeviceMessageService messageService, | |
| 69 | + IotModbusFrameEncoder frameEncoder, | |
| 70 | + IotModbusTcpServerConnectionManager connectionManager, | |
| 71 | + IotModbusTcpServerConfigCacheService configCacheService, | |
| 72 | + IotModbusTcpServerPendingRequestManager pendingRequestManager, | |
| 73 | + IotModbusTcpServerPollScheduler pollScheduler, | |
| 74 | + IotDeviceService deviceService, | |
| 75 | + String serverId) { | |
| 76 | + this.deviceApi = deviceApi; | |
| 77 | + this.messageService = messageService; | |
| 78 | + this.frameEncoder = frameEncoder; | |
| 79 | + this.connectionManager = connectionManager; | |
| 80 | + this.configCacheService = configCacheService; | |
| 81 | + this.pendingRequestManager = pendingRequestManager; | |
| 82 | + this.pollScheduler = pollScheduler; | |
| 83 | + this.deviceService = deviceService; | |
| 84 | + this.serverId = serverId; | |
| 85 | + } | |
| 86 | + | |
| 87 | + // ========== 帧处理入口 ========== | |
| 88 | + | |
| 89 | + /** | |
| 90 | + * 处理帧 | |
| 91 | + */ | |
| 92 | + public void handleFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { | |
| 93 | + if (frame == null) { | |
| 94 | + return; | |
| 95 | + } | |
| 96 | + // 1. 异常响应 | |
| 97 | + if (frame.isException()) { | |
| 98 | + log.warn("[handleFrame][设备异常响应, slaveId={}, FC={}, exceptionCode={}]", | |
| 99 | + frame.getSlaveId(), frame.getFunctionCode(), frame.getExceptionCode()); | |
| 100 | + return; | |
| 101 | + } | |
| 102 | + | |
| 103 | + // 2. 情况一:自定义功能码(认证等扩展) | |
| 104 | + if (StrUtil.isNotEmpty(frame.getCustomData())) { | |
| 105 | + handleCustomFrame(socket, frame, frameFormat); | |
| 106 | + return; | |
| 107 | + } | |
| 108 | + | |
| 109 | + // 3. 情况二:标准 Modbus 响应 → 轮询响应处理 | |
| 110 | + handlePollingResponse(socket, frame, frameFormat); | |
| 111 | + } | |
| 112 | + | |
| 113 | + // ========== 自定义 FC 处理(认证等) ========== | |
| 114 | + | |
| 115 | + /** | |
| 116 | + * 处理自定义功能码帧 | |
| 117 | + * <p> | |
| 118 | + * 异常分层翻译,参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAbstractHandler} | |
| 119 | + */ | |
| 120 | + private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { | |
| 121 | + String method = null; | |
| 122 | + try { | |
| 123 | + IotDeviceMessage message = JsonUtils.parseObject(frame.getCustomData(), IotDeviceMessage.class); | |
| 124 | + if (message == null) { | |
| 125 | + throw invalidParamException("自定义 FC 数据解析失败"); | |
| 126 | + } | |
| 127 | + method = message.getMethod(); | |
| 128 | + if (METHOD_AUTH.equals(method)) { | |
| 129 | + handleAuth(socket, frame, frameFormat, message.getParams()); | |
| 130 | + return; | |
| 131 | + } | |
| 132 | + log.warn("[handleCustomFrame][未知 method: {}, frame: slaveId={}, FC={}, customData={}]", | |
| 133 | + method, frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData()); | |
| 134 | + } catch (ServiceException e) { | |
| 135 | + // 已知业务异常,返回对应的错误码和错误信息 | |
| 136 | + sendCustomResponse(socket, frame, frameFormat, method, e.getCode(), e.getMessage()); | |
| 137 | + } catch (IllegalArgumentException e) { | |
| 138 | + // 参数校验异常,返回 400 错误 | |
| 139 | + sendCustomResponse(socket, frame, frameFormat, method, BAD_REQUEST.getCode(), e.getMessage()); | |
| 140 | + } catch (Exception e) { | |
| 141 | + // 其他未知异常,返回 500 错误 | |
| 142 | + log.error("[handleCustomFrame][解析自定义 FC 数据失败, frame: slaveId={}, FC={}, customData={}]", | |
| 143 | + frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData(), e); | |
| 144 | + sendCustomResponse(socket, frame, frameFormat, method, | |
| 145 | + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | |
| 146 | + } | |
| 147 | + } | |
| 148 | + | |
| 149 | + /** | |
| 150 | + * 处理认证请求 | |
| 151 | + */ | |
| 152 | + @SuppressWarnings("DataFlowIssue") | |
| 153 | + private void handleAuth(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat, Object params) { | |
| 154 | + // 1. 解析认证参数 | |
| 155 | + IotDeviceAuthReqDTO request = JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); | |
| 156 | + Assert.notNull(request, "认证参数不能为空"); | |
| 157 | + Assert.notBlank(request.getUsername(), "username 不能为空"); | |
| 158 | + Assert.notBlank(request.getPassword(), "password 不能为空"); | |
| 159 | + // 特殊:考虑到 modbus 消息体积较小,默认 clientId 传递空串 | |
| 160 | + if (StrUtil.isBlank(request.getClientId())) { | |
| 161 | + request.setClientId(IotDeviceAuthUtils.buildClientIdFromUsername(request.getUsername())); | |
| 162 | + } | |
| 163 | + Assert.notBlank(request.getClientId(), "clientId 不能为空"); | |
| 164 | + | |
| 165 | + // 2.1 调用认证 API | |
| 166 | + CommonResult<Boolean> result = deviceApi.authDevice(request); | |
| 167 | + result.checkError(); | |
| 168 | + if (BooleanUtil.isFalse(result.getData())) { | |
| 169 | + log.warn("[handleAuth][认证失败, clientId={}, username={}]", request.getClientId(), request.getUsername()); | |
| 170 | + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), "认证失败"); | |
| 171 | + return; | |
| 172 | + } | |
| 173 | + // 2.2 解析设备信息 | |
| 174 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(request.getUsername()); | |
| 175 | + Assert.notNull(deviceInfo, "解析设备信息失败"); | |
| 176 | + // 2.3 获取设备信息 | |
| 177 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | |
| 178 | + Assert.notNull(device, "设备不存在"); | |
| 179 | + // 2.4 加载设备 Modbus 配置,无配置则阻断认证 | |
| 180 | + IotModbusDeviceConfigRespDTO modbusConfig = configCacheService.loadDeviceConfig(device.getId()); | |
| 181 | + if (modbusConfig == null) { | |
| 182 | + log.warn("[handleAuth][设备 {} 没有 Modbus 点位配置, 拒绝认证]", device.getId()); | |
| 183 | + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), "设备无 Modbus 配置"); | |
| 184 | + return; | |
| 185 | + } | |
| 186 | + // 2.5 协议不一致,阻断认证 | |
| 187 | + if (ObjUtil.notEqual(frameFormat.getFormat(), modbusConfig.getFrameFormat())) { | |
| 188 | + log.warn("[handleAuth][设备 {} frameFormat 不一致, 连接协议={}, 设备配置={},拒绝认证]", | |
| 189 | + device.getId(), frameFormat.getFormat(), modbusConfig.getFrameFormat()); | |
| 190 | + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), | |
| 191 | + "frameFormat 协议不一致"); | |
| 192 | + return; | |
| 193 | + } | |
| 194 | + | |
| 195 | + // 3.1 注册连接 | |
| 196 | + ConnectionInfo connectionInfo = new ConnectionInfo() | |
| 197 | + .setDeviceId(device.getId()) | |
| 198 | + .setProductKey(deviceInfo.getProductKey()) | |
| 199 | + .setDeviceName(deviceInfo.getDeviceName()) | |
| 200 | + .setSlaveId(frame.getSlaveId()) | |
| 201 | + .setFrameFormat(frameFormat); | |
| 202 | + connectionManager.registerConnection(socket, connectionInfo); | |
| 203 | + // 3.2 发送上线消息 | |
| 204 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 205 | + messageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); | |
| 206 | + // 3.3 发送成功响应 | |
| 207 | + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, | |
| 208 | + GlobalErrorCodeConstants.SUCCESS.getCode(), "success"); | |
| 209 | + log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", request.getClientId(), device.getId()); | |
| 210 | + | |
| 211 | + // 4. 启动轮询 | |
| 212 | + pollScheduler.updatePolling(modbusConfig); | |
| 213 | + } | |
| 214 | + | |
| 215 | + /** | |
| 216 | + * 发送自定义功能码响应 | |
| 217 | + */ | |
| 218 | + private void sendCustomResponse(NetSocket socket, IotModbusFrame frame, | |
| 219 | + IotModbusFrameFormatEnum frameFormat, | |
| 220 | + String method, int code, String message) { | |
| 221 | + Map<String, Object> response = MapUtil.<String, Object>builder() | |
| 222 | + .put("method", method) | |
| 223 | + .put("code", code) | |
| 224 | + .put("message", message) | |
| 225 | + .build(); | |
| 226 | + byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), JsonUtils.toJsonString(response), | |
| 227 | + frameFormat, frame.getTransactionId()); | |
| 228 | + connectionManager.sendToSocket(socket, data); | |
| 229 | + } | |
| 230 | + | |
| 231 | + // ========== 轮询响应处理 ========== | |
| 232 | + | |
| 233 | + /** | |
| 234 | + * 处理轮询响应(云端轮询模式) | |
| 235 | + */ | |
| 236 | + private void handlePollingResponse(NetSocket socket, IotModbusFrame frame, | |
| 237 | + IotModbusFrameFormatEnum frameFormat) { | |
| 238 | + // 1. 获取连接信息(未认证连接丢弃) | |
| 239 | + ConnectionInfo info = connectionManager.getConnectionInfo(socket); | |
| 240 | + if (info == null) { | |
| 241 | + log.warn("[handlePollingResponse][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress()); | |
| 242 | + return; | |
| 243 | + } | |
| 244 | + | |
| 245 | + // 2.1 匹配 PendingRequest | |
| 246 | + PendingRequest request = pendingRequestManager.matchResponse( | |
| 247 | + info.getDeviceId(), frame, frameFormat); | |
| 248 | + if (request == null) { | |
| 249 | + log.debug("[handlePollingResponse][未匹配到 PendingRequest, deviceId={}, FC={}]", | |
| 250 | + info.getDeviceId(), frame.getFunctionCode()); | |
| 251 | + return; | |
| 252 | + } | |
| 253 | + // 2.2 提取寄存器值 | |
| 254 | + int[] rawValues = IotModbusCommonUtils.extractValues(frame); | |
| 255 | + if (rawValues == null) { | |
| 256 | + log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]", | |
| 257 | + info.getDeviceId(), request.getIdentifier()); | |
| 258 | + return; | |
| 259 | + } | |
| 260 | + // 2.3 查找点位配置 | |
| 261 | + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId()); | |
| 262 | + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, request.getPointId()); | |
| 263 | + if (point == null) { | |
| 264 | + return; | |
| 265 | + } | |
| 266 | + | |
| 267 | + // 3.1 转换原始值为物模型属性值(点位翻译) | |
| 268 | + Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValues, point); | |
| 269 | + // 3.2 构造属性上报消息 | |
| 270 | + Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue); | |
| 271 | + IotDeviceMessage message = IotDeviceMessage.requestOf( | |
| 272 | + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); | |
| 273 | + | |
| 274 | + // 4. 发送到消息总线 | |
| 275 | + messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId); | |
| 276 | + log.debug("[handlePollingResponse][设备={}, 属性={}, 原始值={}, 转换值={}]", | |
| 277 | + info.getDeviceId(), request.getIdentifier(), rawValues, convertedValue); | |
| 278 | + } | |
| 279 | + | |
| 280 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | |
| 5 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 9 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; | |
| 10 | +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | |
| 11 | +import lombok.RequiredArgsConstructor; | |
| 12 | +import lombok.extern.slf4j.Slf4j; | |
| 13 | + | |
| 14 | +import java.util.Collections; | |
| 15 | +import java.util.List; | |
| 16 | +import java.util.Map; | |
| 17 | +import java.util.Set; | |
| 18 | +import java.util.concurrent.ConcurrentHashMap; | |
| 19 | + | |
| 20 | +/** | |
| 21 | + * IoT Modbus TCP Server 配置缓存:认证时按需加载,断连时清理,定时刷新已连接设备 | |
| 22 | + * | |
| 23 | + * @author 芋道源码 | |
| 24 | + */ | |
| 25 | +@RequiredArgsConstructor | |
| 26 | +@Slf4j | |
| 27 | +public class IotModbusTcpServerConfigCacheService { | |
| 28 | + | |
| 29 | + private final IotDeviceCommonApi deviceApi; | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 配置缓存:deviceId -> 配置 | |
| 33 | + */ | |
| 34 | + private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>(); | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * 加载单个设备的配置(认证成功后调用) | |
| 38 | + * | |
| 39 | + * @param deviceId 设备 ID | |
| 40 | + * @return 设备配置 | |
| 41 | + */ | |
| 42 | + public IotModbusDeviceConfigRespDTO loadDeviceConfig(Long deviceId) { | |
| 43 | + try { | |
| 44 | + // 1. 从远程 API 获取配置 | |
| 45 | + IotModbusDeviceConfigListReqDTO reqDTO = new IotModbusDeviceConfigListReqDTO() | |
| 46 | + .setStatus(CommonStatusEnum.ENABLE.getStatus()) | |
| 47 | + .setMode(IotModbusModeEnum.POLLING.getMode()) | |
| 48 | + .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SERVER.getType()) | |
| 49 | + .setDeviceIds(Collections.singleton(deviceId)); | |
| 50 | + CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(reqDTO); | |
| 51 | + result.checkError(); | |
| 52 | + IotModbusDeviceConfigRespDTO modbusConfig = CollUtil.getFirst(result.getData()); | |
| 53 | + if (modbusConfig == null) { | |
| 54 | + log.warn("[loadDeviceConfig][远程获取配置失败,未找到数据, deviceId={}]", deviceId); | |
| 55 | + return null; | |
| 56 | + } | |
| 57 | + | |
| 58 | + // 2. 更新缓存并返回 | |
| 59 | + configCache.put(modbusConfig.getDeviceId(), modbusConfig); | |
| 60 | + return modbusConfig; | |
| 61 | + } catch (Exception e) { | |
| 62 | + log.error("[loadDeviceConfig][从远程获取配置失败, deviceId={}]", deviceId, e); | |
| 63 | + return null; | |
| 64 | + } | |
| 65 | + } | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * 刷新已连接设备的配置缓存 | |
| 69 | + * <p> | |
| 70 | + * 定时调用,从远程 API 拉取最新配置,只更新已连接设备的缓存。 | |
| 71 | + * | |
| 72 | + * @param connectedDeviceIds 当前已连接的设备 ID 集合 | |
| 73 | + * @return 已连接设备的最新配置列表 | |
| 74 | + */ | |
| 75 | + public List<IotModbusDeviceConfigRespDTO> refreshConnectedDeviceConfigList(Set<Long> connectedDeviceIds) { | |
| 76 | + if (CollUtil.isEmpty(connectedDeviceIds)) { | |
| 77 | + return Collections.emptyList(); | |
| 78 | + } | |
| 79 | + try { | |
| 80 | + // 1. 从远程获取已连接设备的配置 | |
| 81 | + CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList( | |
| 82 | + new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus()) | |
| 83 | + .setMode(IotModbusModeEnum.POLLING.getMode()) | |
| 84 | + .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SERVER.getType()) | |
| 85 | + .setDeviceIds(connectedDeviceIds)); | |
| 86 | + List<IotModbusDeviceConfigRespDTO> modbusConfigs = result.getCheckedData(); | |
| 87 | + | |
| 88 | + // 2. 更新缓存并返回 | |
| 89 | + for (IotModbusDeviceConfigRespDTO config : modbusConfigs) { | |
| 90 | + configCache.put(config.getDeviceId(), config); | |
| 91 | + } | |
| 92 | + return modbusConfigs; | |
| 93 | + } catch (Exception e) { | |
| 94 | + log.error("[refreshConnectedDeviceConfigList][刷新配置失败]", e); | |
| 95 | + return null; | |
| 96 | + } | |
| 97 | + } | |
| 98 | + | |
| 99 | + /** | |
| 100 | + * 获取设备配置 | |
| 101 | + */ | |
| 102 | + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { | |
| 103 | + IotModbusDeviceConfigRespDTO config = configCache.get(deviceId); | |
| 104 | + if (config != null) { | |
| 105 | + return config; | |
| 106 | + } | |
| 107 | + // 缓存未命中,从远程 API 获取 | |
| 108 | + return loadDeviceConfig(deviceId); | |
| 109 | + } | |
| 110 | + | |
| 111 | + /** | |
| 112 | + * 移除设备配置缓存(设备断连时调用) | |
| 113 | + */ | |
| 114 | + public void removeConfig(Long deviceId) { | |
| 115 | + configCache.remove(deviceId); | |
| 116 | + } | |
| 117 | + | |
| 118 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | |
| 4 | +import io.vertx.core.Future; | |
| 5 | +import io.vertx.core.buffer.Buffer; | |
| 6 | +import io.vertx.core.net.NetSocket; | |
| 7 | +import lombok.Data; | |
| 8 | +import lombok.experimental.Accessors; | |
| 9 | +import lombok.extern.slf4j.Slf4j; | |
| 10 | + | |
| 11 | +import java.util.ArrayList; | |
| 12 | +import java.util.List; | |
| 13 | +import java.util.Map; | |
| 14 | +import java.util.Set; | |
| 15 | +import java.util.concurrent.ConcurrentHashMap; | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * IoT Modbus TCP Server 连接管理器 | |
| 19 | + * <p> | |
| 20 | + * 管理设备 TCP 连接:socket ↔ 设备双向映射 | |
| 21 | + * | |
| 22 | + * @author 芋道源码 | |
| 23 | + */ | |
| 24 | +@Slf4j | |
| 25 | +public class IotModbusTcpServerConnectionManager { | |
| 26 | + | |
| 27 | + /** | |
| 28 | + * socket → 连接信息 | |
| 29 | + */ | |
| 30 | + private final Map<NetSocket, ConnectionInfo> connectionMap = new ConcurrentHashMap<>(); | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * deviceId → socket | |
| 34 | + */ | |
| 35 | + private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>(); | |
| 36 | + | |
| 37 | + /** | |
| 38 | + * 连接信息 | |
| 39 | + */ | |
| 40 | + @Data | |
| 41 | + @Accessors(chain = true) | |
| 42 | + public static class ConnectionInfo { | |
| 43 | + | |
| 44 | + /** | |
| 45 | + * 设备编号 | |
| 46 | + */ | |
| 47 | + private Long deviceId; | |
| 48 | + /** | |
| 49 | + * 产品标识 | |
| 50 | + */ | |
| 51 | + private String productKey; | |
| 52 | + /** | |
| 53 | + * 设备名称 | |
| 54 | + */ | |
| 55 | + private String deviceName; | |
| 56 | + /** | |
| 57 | + * 从站地址 | |
| 58 | + */ | |
| 59 | + private Integer slaveId; | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * 帧格式(首帧自动检测得到) | |
| 63 | + */ | |
| 64 | + private IotModbusFrameFormatEnum frameFormat; | |
| 65 | + | |
| 66 | + } | |
| 67 | + | |
| 68 | + /** | |
| 69 | + * 注册已认证的连接 | |
| 70 | + */ | |
| 71 | + public void registerConnection(NetSocket socket, ConnectionInfo info) { | |
| 72 | + // 先检查该设备是否有旧连接,若有且不是同一个 socket,关闭旧 socket | |
| 73 | + NetSocket oldSocket = deviceSocketMap.get(info.getDeviceId()); | |
| 74 | + if (oldSocket != null && oldSocket != socket) { | |
| 75 | + log.info("[registerConnection][设备 {} 存在旧连接, 关闭旧 socket, oldRemote={}, newRemote={}]", | |
| 76 | + info.getDeviceId(), oldSocket.remoteAddress(), socket.remoteAddress()); | |
| 77 | + connectionMap.remove(oldSocket); | |
| 78 | + try { | |
| 79 | + oldSocket.close(); | |
| 80 | + } catch (Exception e) { | |
| 81 | + log.warn("[registerConnection][关闭旧 socket 失败, deviceId={}, oldRemote={}]", | |
| 82 | + info.getDeviceId(), oldSocket.remoteAddress(), e); | |
| 83 | + } | |
| 84 | + } | |
| 85 | + | |
| 86 | + // 注册新连接 | |
| 87 | + connectionMap.put(socket, info); | |
| 88 | + deviceSocketMap.put(info.getDeviceId(), socket); | |
| 89 | + log.info("[registerConnection][设备 {} 连接已注册, remoteAddress={}]", | |
| 90 | + info.getDeviceId(), socket.remoteAddress()); | |
| 91 | + } | |
| 92 | + | |
| 93 | + /** | |
| 94 | + * 获取连接信息 | |
| 95 | + */ | |
| 96 | + public ConnectionInfo getConnectionInfo(NetSocket socket) { | |
| 97 | + return connectionMap.get(socket); | |
| 98 | + } | |
| 99 | + | |
| 100 | + /** | |
| 101 | + * 根据设备 ID 获取连接信息 | |
| 102 | + */ | |
| 103 | + public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) { | |
| 104 | + NetSocket socket = deviceSocketMap.get(deviceId); | |
| 105 | + return socket != null ? connectionMap.get(socket) : null; | |
| 106 | + } | |
| 107 | + | |
| 108 | + /** | |
| 109 | + * 获取所有已连接设备的 ID 集合 | |
| 110 | + */ | |
| 111 | + public Set<Long> getConnectedDeviceIds() { | |
| 112 | + return deviceSocketMap.keySet(); | |
| 113 | + } | |
| 114 | + | |
| 115 | + /** | |
| 116 | + * 移除连接 | |
| 117 | + */ | |
| 118 | + public ConnectionInfo removeConnection(NetSocket socket) { | |
| 119 | + ConnectionInfo info = connectionMap.remove(socket); | |
| 120 | + if (info != null && info.getDeviceId() != null) { | |
| 121 | + // 使用两参数 remove:只有当 deviceSocketMap 中对应的 socket 就是当前 socket 时才删除, | |
| 122 | + // 避免新 socket 已注册后旧 socket 关闭时误删新映射 | |
| 123 | + boolean removed = deviceSocketMap.remove(info.getDeviceId(), socket); | |
| 124 | + if (removed) { | |
| 125 | + log.info("[removeConnection][设备 {} 连接已移除]", info.getDeviceId()); | |
| 126 | + } else { | |
| 127 | + log.info("[removeConnection][设备 {} 旧连接关闭, 新连接仍在线, 跳过清理]", info.getDeviceId()); | |
| 128 | + } | |
| 129 | + } | |
| 130 | + return info; | |
| 131 | + } | |
| 132 | + | |
| 133 | + /** | |
| 134 | + * 发送数据到设备 | |
| 135 | + * | |
| 136 | + * @return 发送结果 Future | |
| 137 | + */ | |
| 138 | + public Future<Void> sendToDevice(Long deviceId, byte[] data) { | |
| 139 | + NetSocket socket = deviceSocketMap.get(deviceId); | |
| 140 | + if (socket == null) { | |
| 141 | + log.warn("[sendToDevice][设备 {} 没有连接]", deviceId); | |
| 142 | + return Future.failedFuture("设备 " + deviceId + " 没有连接"); | |
| 143 | + } | |
| 144 | + return sendToSocket(socket, data); | |
| 145 | + } | |
| 146 | + | |
| 147 | + /** | |
| 148 | + * 发送数据到指定 socket | |
| 149 | + * | |
| 150 | + * @return 发送结果 Future | |
| 151 | + */ | |
| 152 | + public Future<Void> sendToSocket(NetSocket socket, byte[] data) { | |
| 153 | + return socket.write(Buffer.buffer(data)); | |
| 154 | + } | |
| 155 | + | |
| 156 | + /** | |
| 157 | + * 关闭所有连接 | |
| 158 | + */ | |
| 159 | + public void closeAll() { | |
| 160 | + // 1. 先复制再清空,避免 closeHandler 回调时并发修改 | |
| 161 | + List<NetSocket> sockets = new ArrayList<>(connectionMap.keySet()); | |
| 162 | + connectionMap.clear(); | |
| 163 | + deviceSocketMap.clear(); | |
| 164 | + // 2. 关闭所有 socket(closeHandler 中 removeConnection 发现 map 为空会安全跳过) | |
| 165 | + for (NetSocket socket : sockets) { | |
| 166 | + try { | |
| 167 | + socket.close(); | |
| 168 | + } catch (Exception e) { | |
| 169 | + log.error("[closeAll][关闭连接失败]", e); | |
| 170 | + } | |
| 171 | + } | |
| 172 | + } | |
| 173 | + | |
| 174 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPendingRequestManager.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; | |
| 7 | +import lombok.AllArgsConstructor; | |
| 8 | +import lombok.Data; | |
| 9 | +import lombok.extern.slf4j.Slf4j; | |
| 10 | + | |
| 11 | +import java.util.Deque; | |
| 12 | +import java.util.Iterator; | |
| 13 | +import java.util.Map; | |
| 14 | +import java.util.concurrent.ConcurrentHashMap; | |
| 15 | +import java.util.concurrent.ConcurrentLinkedDeque; | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * IoT Modbus TCP Server 待响应请求管理器 | |
| 19 | + * <p> | |
| 20 | + * 管理轮询下发的请求,用于匹配设备响应: | |
| 21 | + * - TCP 模式:按 transactionId 精确匹配 | |
| 22 | + * - RTU 模式:按 slaveId + functionCode FIFO 匹配 | |
| 23 | + * | |
| 24 | + * @author 芋道源码 | |
| 25 | + */ | |
| 26 | +@Slf4j | |
| 27 | +public class IotModbusTcpServerPendingRequestManager { | |
| 28 | + | |
| 29 | + /** | |
| 30 | + * deviceId → 有序队列 | |
| 31 | + */ | |
| 32 | + private final Map<Long, Deque<PendingRequest>> pendingRequests = new ConcurrentHashMap<>(); | |
| 33 | + | |
| 34 | + /** | |
| 35 | + * 待响应请求信息 | |
| 36 | + */ | |
| 37 | + @Data | |
| 38 | + @AllArgsConstructor | |
| 39 | + public static class PendingRequest { | |
| 40 | + | |
| 41 | + private Long deviceId; | |
| 42 | + private Long pointId; | |
| 43 | + private String identifier; | |
| 44 | + private int slaveId; | |
| 45 | + private int functionCode; | |
| 46 | + private int registerAddress; | |
| 47 | + private int registerCount; | |
| 48 | + private Integer transactionId; | |
| 49 | + private long expireAt; | |
| 50 | + | |
| 51 | + } | |
| 52 | + | |
| 53 | + /** | |
| 54 | + * 添加待响应请求 | |
| 55 | + */ | |
| 56 | + public void addRequest(PendingRequest request) { | |
| 57 | + pendingRequests.computeIfAbsent(request.getDeviceId(), k -> new ConcurrentLinkedDeque<>()) | |
| 58 | + .addLast(request); | |
| 59 | + } | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * 匹配响应(TCP 模式按 transactionId,RTU 模式按 FIFO) | |
| 63 | + * | |
| 64 | + * @param deviceId 设备 ID | |
| 65 | + * @param frame 收到的响应帧 | |
| 66 | + * @param frameFormat 帧格式 | |
| 67 | + * @return 匹配到的 PendingRequest,没有匹配返回 null | |
| 68 | + */ | |
| 69 | + public PendingRequest matchResponse(Long deviceId, IotModbusFrame frame, | |
| 70 | + IotModbusFrameFormatEnum frameFormat) { | |
| 71 | + Deque<PendingRequest> queue = pendingRequests.get(deviceId); | |
| 72 | + if (CollUtil.isEmpty(queue)) { | |
| 73 | + return null; | |
| 74 | + } | |
| 75 | + | |
| 76 | + // TCP 模式:按 transactionId 精确匹配 | |
| 77 | + if (frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP && frame.getTransactionId() != null) { | |
| 78 | + return matchByTransactionId(queue, frame.getTransactionId()); | |
| 79 | + } | |
| 80 | + // RTU 模式:FIFO,匹配 slaveId + functionCode + registerCount | |
| 81 | + int responseRegisterCount = IotModbusCommonUtils.extractRegisterCountFromResponse(frame); | |
| 82 | + return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode(), responseRegisterCount); | |
| 83 | + } | |
| 84 | + | |
| 85 | + /** | |
| 86 | + * 按 transactionId 匹配 | |
| 87 | + */ | |
| 88 | + private PendingRequest matchByTransactionId(Deque<PendingRequest> queue, int transactionId) { | |
| 89 | + Iterator<PendingRequest> it = queue.iterator(); | |
| 90 | + while (it.hasNext()) { | |
| 91 | + PendingRequest req = it.next(); | |
| 92 | + if (req.getTransactionId() != null && req.getTransactionId() == transactionId) { | |
| 93 | + it.remove(); | |
| 94 | + return req; | |
| 95 | + } | |
| 96 | + } | |
| 97 | + return null; | |
| 98 | + } | |
| 99 | + | |
| 100 | + /** | |
| 101 | + * 按 FIFO 匹配(slaveId + functionCode + registerCount) | |
| 102 | + */ | |
| 103 | + private PendingRequest matchByFifo(Deque<PendingRequest> queue, int slaveId, int functionCode, | |
| 104 | + int responseRegisterCount) { | |
| 105 | + Iterator<PendingRequest> it = queue.iterator(); | |
| 106 | + while (it.hasNext()) { | |
| 107 | + PendingRequest req = it.next(); | |
| 108 | + if (req.getSlaveId() == slaveId | |
| 109 | + && req.getFunctionCode() == functionCode | |
| 110 | + && (responseRegisterCount <= 0 || req.getRegisterCount() == responseRegisterCount)) { | |
| 111 | + it.remove(); | |
| 112 | + return req; | |
| 113 | + } | |
| 114 | + } | |
| 115 | + return null; | |
| 116 | + } | |
| 117 | + | |
| 118 | + /** | |
| 119 | + * 清理过期请求 | |
| 120 | + */ | |
| 121 | + public void cleanupExpired() { | |
| 122 | + long now = System.currentTimeMillis(); | |
| 123 | + for (Map.Entry<Long, Deque<PendingRequest>> entry : pendingRequests.entrySet()) { | |
| 124 | + Deque<PendingRequest> queue = entry.getValue(); | |
| 125 | + int removed = 0; | |
| 126 | + Iterator<PendingRequest> it = queue.iterator(); | |
| 127 | + while (it.hasNext()) { | |
| 128 | + PendingRequest req = it.next(); | |
| 129 | + if (req.getExpireAt() < now) { | |
| 130 | + it.remove(); | |
| 131 | + removed++; | |
| 132 | + } | |
| 133 | + } | |
| 134 | + if (removed > 0) { | |
| 135 | + log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed); | |
| 136 | + } | |
| 137 | + } | |
| 138 | + } | |
| 139 | + | |
| 140 | + /** | |
| 141 | + * 清理指定设备的所有待响应请求 | |
| 142 | + */ | |
| 143 | + public void removeDevice(Long deviceId) { | |
| 144 | + pendingRequests.remove(deviceId); | |
| 145 | + } | |
| 146 | + | |
| 147 | + /** | |
| 148 | + * 清理所有待响应请求 | |
| 149 | + */ | |
| 150 | + public void clear() { | |
| 151 | + pendingRequests.clear(); | |
| 152 | + } | |
| 153 | + | |
| 154 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPollScheduler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.lang.Assert; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager.PendingRequest; | |
| 13 | +import io.vertx.core.Vertx; | |
| 14 | +import lombok.Getter; | |
| 15 | +import lombok.extern.slf4j.Slf4j; | |
| 16 | + | |
| 17 | +import java.util.concurrent.atomic.AtomicInteger; | |
| 18 | + | |
| 19 | +/** | |
| 20 | + * IoT Modbus TCP Server 轮询调度器:编码读请求帧,通过 TCP 连接发送到设备,注册 PendingRequest 等待响应 | |
| 21 | + * | |
| 22 | + * @author 芋道源码 | |
| 23 | + */ | |
| 24 | +@Slf4j | |
| 25 | +public class IotModbusTcpServerPollScheduler extends AbstractIotModbusPollScheduler { | |
| 26 | + | |
| 27 | + private final IotModbusTcpServerConnectionManager connectionManager; | |
| 28 | + private final IotModbusFrameEncoder frameEncoder; | |
| 29 | + private final IotModbusTcpServerPendingRequestManager pendingRequestManager; | |
| 30 | + private final IotModbusTcpServerConfigCacheService configCacheService; | |
| 31 | + private final int requestTimeout; | |
| 32 | + /** | |
| 33 | + * TCP 事务 ID 自增器(与 DownstreamHandler 共享) | |
| 34 | + */ | |
| 35 | + @Getter | |
| 36 | + private final AtomicInteger transactionIdCounter; | |
| 37 | + | |
| 38 | + public IotModbusTcpServerPollScheduler(Vertx vertx, | |
| 39 | + IotModbusTcpServerConnectionManager connectionManager, | |
| 40 | + IotModbusFrameEncoder frameEncoder, | |
| 41 | + IotModbusTcpServerPendingRequestManager pendingRequestManager, | |
| 42 | + int requestTimeout, | |
| 43 | + AtomicInteger transactionIdCounter, | |
| 44 | + IotModbusTcpServerConfigCacheService configCacheService) { | |
| 45 | + super(vertx); | |
| 46 | + this.connectionManager = connectionManager; | |
| 47 | + this.frameEncoder = frameEncoder; | |
| 48 | + this.pendingRequestManager = pendingRequestManager; | |
| 49 | + this.requestTimeout = requestTimeout; | |
| 50 | + this.transactionIdCounter = transactionIdCounter; | |
| 51 | + this.configCacheService = configCacheService; | |
| 52 | + } | |
| 53 | + | |
| 54 | + // ========== 轮询执行 ========== | |
| 55 | + | |
| 56 | + /** | |
| 57 | + * 轮询单个点位 | |
| 58 | + */ | |
| 59 | + @Override | |
| 60 | + @SuppressWarnings("DuplicatedCode") | |
| 61 | + protected void pollPoint(Long deviceId, Long pointId) { | |
| 62 | + // 1.1 从 configCache 获取最新配置 | |
| 63 | + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); | |
| 64 | + if (config == null || CollUtil.isEmpty(config.getPoints())) { | |
| 65 | + log.warn("[pollPoint][设备 {} 没有配置]", deviceId); | |
| 66 | + return; | |
| 67 | + } | |
| 68 | + // 1.2 查找点位 | |
| 69 | + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId); | |
| 70 | + if (point == null) { | |
| 71 | + log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId); | |
| 72 | + return; | |
| 73 | + } | |
| 74 | + | |
| 75 | + // 2.1 获取连接 | |
| 76 | + ConnectionInfo connection = connectionManager.getConnectionInfoByDeviceId(deviceId); | |
| 77 | + if (connection == null) { | |
| 78 | + log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId); | |
| 79 | + return; | |
| 80 | + } | |
| 81 | + // 2.2 获取 slave ID | |
| 82 | + IotModbusFrameFormatEnum frameFormat = connection.getFrameFormat(); | |
| 83 | + Assert.notNull(frameFormat, "设备 {} 的帧格式不能为空", deviceId); | |
| 84 | + Integer slaveId = connection.getSlaveId(); | |
| 85 | + Assert.notNull(connection.getSlaveId(), "设备 {} 的 slaveId 不能为空", deviceId); | |
| 86 | + | |
| 87 | + // 3.1 编码读请求 | |
| 88 | + Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP | |
| 89 | + ? (transactionIdCounter.incrementAndGet() & 0xFFFF) | |
| 90 | + : null; | |
| 91 | + byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(), | |
| 92 | + point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId); | |
| 93 | + // 3.2 注册 PendingRequest | |
| 94 | + PendingRequest pendingRequest = new PendingRequest( | |
| 95 | + deviceId, point.getId(), point.getIdentifier(), | |
| 96 | + slaveId, point.getFunctionCode(), | |
| 97 | + point.getRegisterAddress(), point.getRegisterCount(), | |
| 98 | + transactionId, | |
| 99 | + System.currentTimeMillis() + requestTimeout); | |
| 100 | + pendingRequestManager.addRequest(pendingRequest); | |
| 101 | + // 3.3 发送读请求 | |
| 102 | + connectionManager.sendToDevice(deviceId, data).onSuccess(v -> | |
| 103 | + log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]", | |
| 104 | + deviceId, point.getIdentifier(), point.getFunctionCode(), | |
| 105 | + point.getRegisterAddress(), point.getRegisterCount()) | |
| 106 | + ).onFailure(e -> | |
| 107 | + log.warn("[pollPoint][发送失败, 设备={}, 点位={}]", deviceId, point.getIdentifier(), e) | |
| 108 | + ); | |
| 109 | + } | |
| 110 | + | |
| 111 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/package-info.java
0 → 100644
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java
| ... | ... | @@ -81,7 +81,7 @@ public class IotMqttProtocol implements IotProtocol { |
| 81 | 81 | /** |
| 82 | 82 | * 下行消息订阅者 |
| 83 | 83 | */ |
| 84 | - private final IotMqttDownstreamSubscriber downstreamSubscriber; | |
| 84 | + private IotMqttDownstreamSubscriber downstreamSubscriber; | |
| 85 | 85 | |
| 86 | 86 | private final IotDeviceMessageService deviceMessageService; |
| 87 | 87 | |
| ... | ... | @@ -104,11 +104,6 @@ public class IotMqttProtocol implements IotProtocol { |
| 104 | 104 | this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId); |
| 105 | 105 | this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService); |
| 106 | 106 | this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId); |
| 107 | - | |
| 108 | - // 初始化下行消息订阅者 | |
| 109 | - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 110 | - IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager); | |
| 111 | - this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 112 | 107 | } |
| 113 | 108 | |
| 114 | 109 | @Override |
| ... | ... | @@ -157,18 +152,13 @@ public class IotMqttProtocol implements IotProtocol { |
| 157 | 152 | getId(), properties.getPort(), serverId); |
| 158 | 153 | |
| 159 | 154 | // 2. 启动下行消息订阅者 |
| 155 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 156 | + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager); | |
| 157 | + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 160 | 158 | this.downstreamSubscriber.start(); |
| 161 | 159 | } catch (Exception e) { |
| 162 | 160 | log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e); |
| 163 | - // 启动失败时关闭资源 | |
| 164 | - if (mqttServer != null) { | |
| 165 | - mqttServer.close(); | |
| 166 | - mqttServer = null; | |
| 167 | - } | |
| 168 | - if (vertx != null) { | |
| 169 | - vertx.close(); | |
| 170 | - vertx = null; | |
| 171 | - } | |
| 161 | + stop0(); | |
| 172 | 162 | throw e; |
| 173 | 163 | } |
| 174 | 164 | } |
| ... | ... | @@ -178,15 +168,24 @@ public class IotMqttProtocol implements IotProtocol { |
| 178 | 168 | if (!running) { |
| 179 | 169 | return; |
| 180 | 170 | } |
| 171 | + stop0(); | |
| 172 | + } | |
| 173 | + | |
| 174 | + private void stop0() { | |
| 181 | 175 | // 1. 停止下行消息订阅者 |
| 182 | - try { | |
| 183 | - downstreamSubscriber.stop(); | |
| 184 | - log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId()); | |
| 185 | - } catch (Exception e) { | |
| 186 | - log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 176 | + if (downstreamSubscriber != null) { | |
| 177 | + try { | |
| 178 | + downstreamSubscriber.stop(); | |
| 179 | + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId()); | |
| 180 | + } catch (Exception e) { | |
| 181 | + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 182 | + } | |
| 183 | + downstreamSubscriber = null; | |
| 187 | 184 | } |
| 188 | 185 | |
| 189 | - // 2.1 关闭 MQTT 服务器 | |
| 186 | + // 2.1 关闭所有连接 | |
| 187 | + connectionManager.closeAll(); | |
| 188 | + // 2.2 关闭 MQTT 服务器 | |
| 190 | 189 | if (mqttServer != null) { |
| 191 | 190 | try { |
| 192 | 191 | mqttServer.close().result(); |
| ... | ... | @@ -196,7 +195,7 @@ public class IotMqttProtocol implements IotProtocol { |
| 196 | 195 | } |
| 197 | 196 | mqttServer = null; |
| 198 | 197 | } |
| 199 | - // 2.2 关闭 Vertx 实例 | |
| 198 | + // 2.3 关闭 Vertx 实例 | |
| 200 | 199 | if (vertx != null) { |
| 201 | 200 | try { |
| 202 | 201 | vertx.close().result(); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java
| ... | ... | @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; |
| 7 | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | |
| ... | ... | @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; |
| 12 | 12 | * @author 芋道源码 |
| 13 | 13 | */ |
| 14 | 14 | @Slf4j |
| 15 | -public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 15 | +public class IotMqttDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { | |
| 16 | 16 | |
| 17 | 17 | private final IotMqttDownstreamHandler downstreamHandler; |
| 18 | 18 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java
| ... | ... | @@ -2,15 +2,13 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.lang.Assert; |
| 4 | 4 | import cn.hutool.core.util.ArrayUtil; |
| 5 | -import cn.iocoder.yudao.framework.common.exception.ServiceException; | |
| 6 | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| 8 | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 9 | 9 | import io.vertx.mqtt.MqttEndpoint; |
| 10 | 10 | import lombok.extern.slf4j.Slf4j; |
| 11 | 11 | |
| 12 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; | |
| 13 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | |
| 14 | 12 | |
| 15 | 13 | /** |
| 16 | 14 | * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等) |
| ... | ... | @@ -38,10 +36,6 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { |
| 38 | 36 | */ |
| 39 | 37 | public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) { |
| 40 | 38 | String clientId = endpoint.clientIdentifier(); |
| 41 | - IotDeviceMessage message = null; | |
| 42 | - String productKey = null; | |
| 43 | - String deviceName = null; | |
| 44 | - | |
| 45 | 39 | try { |
| 46 | 40 | // 1.1 基础检查 |
| 47 | 41 | if (ArrayUtil.isEmpty(payload)) { |
| ... | ... | @@ -49,8 +43,8 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { |
| 49 | 43 | } |
| 50 | 44 | // 1.2 解析主题,获取 productKey 和 deviceName |
| 51 | 45 | String[] topicParts = topic.split("/"); |
| 52 | - productKey = ArrayUtil.get(topicParts, 2); | |
| 53 | - deviceName = ArrayUtil.get(topicParts, 3); | |
| 46 | + String productKey = ArrayUtil.get(topicParts, 2); | |
| 47 | + String deviceName = ArrayUtil.get(topicParts, 3); | |
| 54 | 48 | Assert.notBlank(productKey, "产品 Key 不能为空"); |
| 55 | 49 | Assert.notBlank(deviceName, "设备名称不能为空"); |
| 56 | 50 | // 1.3 校验设备信息,防止伪造设备消息 |
| ... | ... | @@ -58,39 +52,27 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { |
| 58 | 52 | Assert.notNull(connectionInfo, "无法获取连接信息"); |
| 59 | 53 | Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配"); |
| 60 | 54 | Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配"); |
| 55 | + // 1.4 校验 topic 是否允许发布 | |
| 56 | + if (!IotMqttTopicUtils.isTopicPublishAllowed(topic, productKey, deviceName)) { | |
| 57 | + log.warn("[handleBusinessRequest][topic 不允许发布,客户端 ID: {},主题: {}]", clientId, topic); | |
| 58 | + return; | |
| 59 | + } | |
| 61 | 60 | |
| 62 | - // 2. 反序列化消息 | |
| 63 | - message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); | |
| 61 | + // 2.1 反序列化消息 | |
| 62 | + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); | |
| 64 | 63 | if (message == null) { |
| 65 | 64 | log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); |
| 66 | - sendErrorResponse(endpoint, productKey, deviceName, null, null, | |
| 67 | - BAD_REQUEST.getCode(), "消息解码失败"); | |
| 68 | 65 | return; |
| 69 | 66 | } |
| 67 | + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀) | |
| 68 | + IotMqttTopicUtils.normalizeReplyMethod(message); | |
| 70 | 69 | |
| 71 | 70 | // 3. 处理业务消息 |
| 72 | 71 | deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); |
| 73 | 72 | log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic); |
| 74 | - } catch (ServiceException e) { | |
| 75 | - log.warn("[handleBusinessRequest][业务异常,客户端 ID: {},主题: {},错误: {}]", | |
| 76 | - clientId, topic, e.getMessage()); | |
| 77 | - String requestId = message != null ? message.getRequestId() : null; | |
| 78 | - String method = message != null ? message.getMethod() : null; | |
| 79 | - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage()); | |
| 80 | - } catch (IllegalArgumentException e) { | |
| 81 | - log.warn("[handleBusinessRequest][参数校验失败,客户端 ID: {},主题: {},错误: {}]", | |
| 82 | - clientId, topic, e.getMessage()); | |
| 83 | - String requestId = message != null ? message.getRequestId() : null; | |
| 84 | - String method = message != null ? message.getMethod() : null; | |
| 85 | - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, | |
| 86 | - BAD_REQUEST.getCode(), e.getMessage()); | |
| 87 | 73 | } catch (Exception e) { |
| 88 | 74 | log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]", |
| 89 | 75 | clientId, topic, e.getMessage(), e); |
| 90 | - String requestId = message != null ? message.getRequestId() : null; | |
| 91 | - String method = message != null ? message.getMethod() : null; | |
| 92 | - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, | |
| 93 | - INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | |
| 94 | 76 | } |
| 95 | 77 | } |
| 96 | 78 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
| ... | ... | @@ -8,6 +8,8 @@ import lombok.Data; |
| 8 | 8 | import lombok.extern.slf4j.Slf4j; |
| 9 | 9 | import org.springframework.stereotype.Component; |
| 10 | 10 | |
| 11 | +import java.util.ArrayList; | |
| 12 | +import java.util.List; | |
| 11 | 13 | import java.util.Map; |
| 12 | 14 | import java.util.concurrent.ConcurrentHashMap; |
| 13 | 15 | |
| ... | ... | @@ -167,6 +169,24 @@ public class IotMqttConnectionManager { |
| 167 | 169 | } |
| 168 | 170 | |
| 169 | 171 | /** |
| 172 | + * 关闭所有连接 | |
| 173 | + */ | |
| 174 | + public void closeAll() { | |
| 175 | + // 1. 先复制再清空,避免 closeHandler 回调时并发修改 | |
| 176 | + List<MqttEndpoint> endpoints = new ArrayList<>(connectionMap.keySet()); | |
| 177 | + connectionMap.clear(); | |
| 178 | + deviceEndpointMap.clear(); | |
| 179 | + // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过) | |
| 180 | + for (MqttEndpoint endpoint : endpoints) { | |
| 181 | + try { | |
| 182 | + endpoint.close(); | |
| 183 | + } catch (Exception ignored) { | |
| 184 | + // 连接可能已关闭,忽略异常 | |
| 185 | + } | |
| 186 | + } | |
| 187 | + } | |
| 188 | + | |
| 189 | + /** | |
| 170 | 190 | * 连接信息 |
| 171 | 191 | */ |
| 172 | 192 | @Data | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java
| ... | ... | @@ -66,7 +66,7 @@ public class IotTcpProtocol implements IotProtocol { |
| 66 | 66 | /** |
| 67 | 67 | * 下行消息订阅者 |
| 68 | 68 | */ |
| 69 | - private final IotTcpDownstreamSubscriber downstreamSubscriber; | |
| 69 | + private IotTcpDownstreamSubscriber downstreamSubscriber; | |
| 70 | 70 | |
| 71 | 71 | /** |
| 72 | 72 | * 消息序列化器 |
| ... | ... | @@ -94,11 +94,6 @@ public class IotTcpProtocol implements IotProtocol { |
| 94 | 94 | |
| 95 | 95 | // 初始化连接管理器 |
| 96 | 96 | this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections()); |
| 97 | - | |
| 98 | - // 初始化下行消息订阅者 | |
| 99 | - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 100 | - IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); | |
| 101 | - this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 102 | 97 | } |
| 103 | 98 | |
| 104 | 99 | @Override |
| ... | ... | @@ -152,18 +147,13 @@ public class IotTcpProtocol implements IotProtocol { |
| 152 | 147 | getId(), properties.getPort(), serverId); |
| 153 | 148 | |
| 154 | 149 | // 2. 启动下行消息订阅者 |
| 150 | + IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); | |
| 151 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 152 | + this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 155 | 153 | this.downstreamSubscriber.start(); |
| 156 | 154 | } catch (Exception e) { |
| 157 | 155 | log.error("[start][IoT TCP 协议 {} 启动失败]", getId(), e); |
| 158 | - // 启动失败时关闭资源 | |
| 159 | - if (tcpServer != null) { | |
| 160 | - tcpServer.close(); | |
| 161 | - tcpServer = null; | |
| 162 | - } | |
| 163 | - if (vertx != null) { | |
| 164 | - vertx.close(); | |
| 165 | - vertx = null; | |
| 166 | - } | |
| 156 | + stop0(); | |
| 167 | 157 | throw e; |
| 168 | 158 | } |
| 169 | 159 | } |
| ... | ... | @@ -173,15 +163,24 @@ public class IotTcpProtocol implements IotProtocol { |
| 173 | 163 | if (!running) { |
| 174 | 164 | return; |
| 175 | 165 | } |
| 166 | + stop0(); | |
| 167 | + } | |
| 168 | + | |
| 169 | + private void stop0() { | |
| 176 | 170 | // 1. 停止下行消息订阅者 |
| 177 | - try { | |
| 178 | - downstreamSubscriber.stop(); | |
| 179 | - log.info("[stop][IoT TCP 协议 {} 下行消息订阅者已停止]", getId()); | |
| 180 | - } catch (Exception e) { | |
| 181 | - log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 171 | + if (downstreamSubscriber != null) { | |
| 172 | + try { | |
| 173 | + downstreamSubscriber.stop(); | |
| 174 | + log.info("[stop][IoT TCP 协议 {} 下行消息订阅者已停止]", getId()); | |
| 175 | + } catch (Exception e) { | |
| 176 | + log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 177 | + } | |
| 178 | + downstreamSubscriber = null; | |
| 182 | 179 | } |
| 183 | 180 | |
| 184 | - // 2.1 关闭 TCP 服务器 | |
| 181 | + // 2.1 关闭所有连接 | |
| 182 | + connectionManager.closeAll(); | |
| 183 | + // 2.2 关闭 TCP 服务器 | |
| 185 | 184 | if (tcpServer != null) { |
| 186 | 185 | try { |
| 187 | 186 | tcpServer.close().result(); |
| ... | ... | @@ -191,7 +190,7 @@ public class IotTcpProtocol implements IotProtocol { |
| 191 | 190 | } |
| 192 | 191 | tcpServer = null; |
| 193 | 192 | } |
| 194 | - // 2.2 关闭 Vertx 实例 | |
| 193 | + // 2.3 关闭 Vertx 实例 | |
| 195 | 194 | if (vertx != null) { |
| 196 | 195 | try { |
| 197 | 196 | vertx.close().result(); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.ObjUtil; | |
| 4 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 3 | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 4 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; |
| 5 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; |
| ... | ... | @@ -33,9 +35,17 @@ public class IotTcpDownstreamHandler { |
| 33 | 35 | */ |
| 34 | 36 | public void handle(IotDeviceMessage message) { |
| 35 | 37 | try { |
| 38 | + // 1.1 检查是否是属性设置消息 | |
| 39 | + if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) { | |
| 40 | + return; | |
| 41 | + } | |
| 42 | + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { | |
| 43 | + log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod()); | |
| 44 | + return; | |
| 45 | + } | |
| 36 | 46 | log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", |
| 37 | 47 | message.getDeviceId(), message.getMethod(), message.getId()); |
| 38 | - // 1. 检查设备连接 | |
| 48 | + // 1.2 检查设备连接 | |
| 39 | 49 | IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( |
| 40 | 50 | message.getDeviceId()); |
| 41 | 51 | if (connectionInfo == null) { | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java
| ... | ... | @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream; |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; | |
| 7 | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | |
| 9 | 9 | /** |
| ... | ... | @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; |
| 12 | 12 | * @author 芋道源码 |
| 13 | 13 | */ |
| 14 | 14 | @Slf4j |
| 15 | -public class IotTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 15 | +public class IotTcpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { | |
| 16 | 16 | |
| 17 | 17 | private final IotTcpDownstreamHandler downstreamHandler; |
| 18 | 18 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
| ... | ... | @@ -5,6 +5,8 @@ import io.vertx.core.net.NetSocket; |
| 5 | 5 | import lombok.Data; |
| 6 | 6 | import lombok.extern.slf4j.Slf4j; |
| 7 | 7 | |
| 8 | +import java.util.ArrayList; | |
| 9 | +import java.util.List; | |
| 8 | 10 | import java.util.Map; |
| 9 | 11 | import java.util.concurrent.ConcurrentHashMap; |
| 10 | 12 | |
| ... | ... | @@ -123,6 +125,24 @@ public class IotTcpConnectionManager { |
| 123 | 125 | } |
| 124 | 126 | |
| 125 | 127 | /** |
| 128 | + * 关闭所有连接 | |
| 129 | + */ | |
| 130 | + public void closeAll() { | |
| 131 | + // 1. 先复制再清空,避免 closeHandler 回调时并发修改 | |
| 132 | + List<NetSocket> sockets = new ArrayList<>(connectionMap.keySet()); | |
| 133 | + connectionMap.clear(); | |
| 134 | + deviceSocketMap.clear(); | |
| 135 | + // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过) | |
| 136 | + for (NetSocket socket : sockets) { | |
| 137 | + try { | |
| 138 | + socket.close(); | |
| 139 | + } catch (Exception ignored) { | |
| 140 | + // 连接可能已关闭,忽略异常 | |
| 141 | + } | |
| 142 | + } | |
| 143 | + } | |
| 144 | + | |
| 145 | + /** | |
| 126 | 146 | * 连接信息(包含认证信息) |
| 127 | 147 | */ |
| 128 | 148 | @Data | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java
| ... | ... | @@ -63,7 +63,7 @@ public class IotUdpProtocol implements IotProtocol { |
| 63 | 63 | /** |
| 64 | 64 | * 下行消息订阅者 |
| 65 | 65 | */ |
| 66 | - private final IotUdpDownstreamSubscriber downstreamSubscriber; | |
| 66 | + private IotUdpDownstreamSubscriber downstreamSubscriber; | |
| 67 | 67 | |
| 68 | 68 | /** |
| 69 | 69 | * 消息序列化器 |
| ... | ... | @@ -85,10 +85,6 @@ public class IotUdpProtocol implements IotProtocol { |
| 85 | 85 | // 初始化会话管理器 |
| 86 | 86 | this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions(), udpConfig.getSessionTimeoutMs()); |
| 87 | 87 | |
| 88 | - // 初始化下行消息订阅者 | |
| 89 | - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 90 | - IotUdpDownstreamHandler downstreamHandler = new IotUdpDownstreamHandler(this, sessionManager, serializer); | |
| 91 | - this.downstreamSubscriber = new IotUdpDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 92 | 88 | } |
| 93 | 89 | |
| 94 | 90 | @Override |
| ... | ... | @@ -108,8 +104,11 @@ public class IotUdpProtocol implements IotProtocol { |
| 108 | 104 | return; |
| 109 | 105 | } |
| 110 | 106 | |
| 111 | - // 1.1 创建 Vertx 实例 | |
| 107 | + // 1.1 创建 Vertx 实例 和 下行消息订阅者 | |
| 112 | 108 | this.vertx = Vertx.vertx(); |
| 109 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 110 | + IotUdpDownstreamHandler downstreamHandler = new IotUdpDownstreamHandler(this, sessionManager, serializer); | |
| 111 | + this.downstreamSubscriber = new IotUdpDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 113 | 112 | |
| 114 | 113 | // 1.2 创建 UDP Socket 选项 |
| 115 | 114 | IotUdpConfig udpConfig = properties.getUdp(); |
| ... | ... | @@ -137,15 +136,7 @@ public class IotUdpProtocol implements IotProtocol { |
| 137 | 136 | this.downstreamSubscriber.start(); |
| 138 | 137 | } catch (Exception e) { |
| 139 | 138 | log.error("[start][IoT UDP 协议 {} 启动失败]", getId(), e); |
| 140 | - // 启动失败时关闭资源 | |
| 141 | - if (udpSocket != null) { | |
| 142 | - udpSocket.close(); | |
| 143 | - udpSocket = null; | |
| 144 | - } | |
| 145 | - if (vertx != null) { | |
| 146 | - vertx.close(); | |
| 147 | - vertx = null; | |
| 148 | - } | |
| 139 | + stop0(); | |
| 149 | 140 | throw e; |
| 150 | 141 | } |
| 151 | 142 | } |
| ... | ... | @@ -155,12 +146,19 @@ public class IotUdpProtocol implements IotProtocol { |
| 155 | 146 | if (!running) { |
| 156 | 147 | return; |
| 157 | 148 | } |
| 149 | + stop0(); | |
| 150 | + } | |
| 151 | + | |
| 152 | + private void stop0() { | |
| 158 | 153 | // 1. 停止下行消息订阅者 |
| 159 | - try { | |
| 160 | - downstreamSubscriber.stop(); | |
| 161 | - log.info("[stop][IoT UDP 协议 {} 下行消息订阅者已停止]", getId()); | |
| 162 | - } catch (Exception e) { | |
| 163 | - log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 154 | + if (downstreamSubscriber != null) { | |
| 155 | + try { | |
| 156 | + downstreamSubscriber.stop(); | |
| 157 | + log.info("[stop][IoT UDP 协议 {} 下行消息订阅者已停止]", getId()); | |
| 158 | + } catch (Exception e) { | |
| 159 | + log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 160 | + } | |
| 161 | + downstreamSubscriber = null; | |
| 164 | 162 | } |
| 165 | 163 | |
| 166 | 164 | // 2.1 关闭 UDP Socket | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java
| ... | ... | @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream; |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; | |
| 7 | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | |
| 9 | 9 | /** |
| ... | ... | @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; |
| 12 | 12 | * @author 芋道源码 |
| 13 | 13 | */ |
| 14 | 14 | @Slf4j |
| 15 | -public class IotUdpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 15 | +public class IotUdpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { | |
| 16 | 16 | |
| 17 | 17 | private final IotUdpDownstreamHandler downstreamHandler; |
| 18 | 18 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 3 | 4 | import cn.hutool.core.util.ObjUtil; |
| 4 | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| ... | ... | @@ -9,8 +10,8 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 9 | 10 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 10 | 11 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; |
| 11 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 12 | -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; | |
| 13 | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler; |
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; | |
| 14 | 15 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.upstream.IotWebSocketUpstreamHandler; |
| 15 | 16 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; |
| 16 | 17 | import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; |
| ... | ... | @@ -21,7 +22,6 @@ import io.vertx.core.http.HttpServerOptions; |
| 21 | 22 | import io.vertx.core.net.PemKeyCertOptions; |
| 22 | 23 | import lombok.Getter; |
| 23 | 24 | import lombok.extern.slf4j.Slf4j; |
| 24 | -import cn.hutool.core.lang.Assert; | |
| 25 | 25 | |
| 26 | 26 | /** |
| 27 | 27 | * IoT WebSocket 协议实现 |
| ... | ... | @@ -65,7 +65,7 @@ public class IotWebSocketProtocol implements IotProtocol { |
| 65 | 65 | /** |
| 66 | 66 | * 下行消息订阅者 |
| 67 | 67 | */ |
| 68 | - private final IotWebSocketDownstreamSubscriber downstreamSubscriber; | |
| 68 | + private IotWebSocketDownstreamSubscriber downstreamSubscriber; | |
| 69 | 69 | |
| 70 | 70 | /** |
| 71 | 71 | * 消息序列化器 |
| ... | ... | @@ -87,10 +87,6 @@ public class IotWebSocketProtocol implements IotProtocol { |
| 87 | 87 | // 初始化连接管理器 |
| 88 | 88 | this.connectionManager = new IotWebSocketConnectionManager(); |
| 89 | 89 | |
| 90 | - // 初始化下行消息订阅者 | |
| 91 | - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 92 | - IotWebSocketDownstreamHandler downstreamHandler = new IotWebSocketDownstreamHandler(serializer, connectionManager); | |
| 93 | - this.downstreamSubscriber = new IotWebSocketDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 94 | 90 | } |
| 95 | 91 | |
| 96 | 92 | @Override |
| ... | ... | @@ -152,17 +148,13 @@ public class IotWebSocketProtocol implements IotProtocol { |
| 152 | 148 | getId(), properties.getPort(), wsConfig.getPath(), serverId); |
| 153 | 149 | |
| 154 | 150 | // 2. 启动下行消息订阅者 |
| 151 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | |
| 152 | + IotWebSocketDownstreamHandler downstreamHandler = new IotWebSocketDownstreamHandler(serializer, connectionManager); | |
| 153 | + this.downstreamSubscriber = new IotWebSocketDownstreamSubscriber(this, downstreamHandler, messageBus); | |
| 155 | 154 | downstreamSubscriber.start(); |
| 156 | 155 | } catch (Exception e) { |
| 157 | 156 | log.error("[start][IoT WebSocket 协议 {} 启动失败]", getId(), e); |
| 158 | - if (httpServer != null) { | |
| 159 | - httpServer.close(); | |
| 160 | - httpServer = null; | |
| 161 | - } | |
| 162 | - if (vertx != null) { | |
| 163 | - vertx.close(); | |
| 164 | - vertx = null; | |
| 165 | - } | |
| 157 | + stop0(); | |
| 166 | 158 | throw e; |
| 167 | 159 | } |
| 168 | 160 | } |
| ... | ... | @@ -172,15 +164,24 @@ public class IotWebSocketProtocol implements IotProtocol { |
| 172 | 164 | if (!running) { |
| 173 | 165 | return; |
| 174 | 166 | } |
| 167 | + stop0(); | |
| 168 | + } | |
| 169 | + | |
| 170 | + private void stop0() { | |
| 175 | 171 | // 1. 停止下行消息订阅者 |
| 176 | - try { | |
| 177 | - downstreamSubscriber.stop(); | |
| 178 | - log.info("[stop][IoT WebSocket 协议 {} 下行消息订阅者已停止]", getId()); | |
| 179 | - } catch (Exception e) { | |
| 180 | - log.error("[stop][IoT WebSocket 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 172 | + if (downstreamSubscriber != null) { | |
| 173 | + try { | |
| 174 | + downstreamSubscriber.stop(); | |
| 175 | + log.info("[stop][IoT WebSocket 协议 {} 下行消息订阅者已停止]", getId()); | |
| 176 | + } catch (Exception e) { | |
| 177 | + log.error("[stop][IoT WebSocket 协议 {} 下行消息订阅者停止失败]", getId(), e); | |
| 178 | + } | |
| 179 | + downstreamSubscriber = null; | |
| 181 | 180 | } |
| 182 | 181 | |
| 183 | - // 2.1 关闭 WebSocket 服务器 | |
| 182 | + // 2.1 关闭所有连接 | |
| 183 | + connectionManager.closeAll(); | |
| 184 | + // 2.2 关闭 WebSocket 服务器 | |
| 184 | 185 | if (httpServer != null) { |
| 185 | 186 | try { |
| 186 | 187 | httpServer.close().result(); |
| ... | ... | @@ -190,7 +191,7 @@ public class IotWebSocketProtocol implements IotProtocol { |
| 190 | 191 | } |
| 191 | 192 | httpServer = null; |
| 192 | 193 | } |
| 193 | - // 2.2 关闭 Vertx 实例 | |
| 194 | + // 2.3 关闭 Vertx 实例 | |
| 194 | 195 | if (vertx != null) { |
| 195 | 196 | try { |
| 196 | 197 | vertx.close().result(); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java
| ... | ... | @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstrea |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; |
| 7 | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | |
| ... | ... | @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; |
| 12 | 12 | * @author 芋道源码 |
| 13 | 13 | */ |
| 14 | 14 | @Slf4j |
| 15 | -public class IotWebSocketDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | |
| 15 | +public class IotWebSocketDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { | |
| 16 | 16 | |
| 17 | 17 | private final IotWebSocketDownstreamHandler downstreamHandler; |
| 18 | 18 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java
| ... | ... | @@ -5,6 +5,8 @@ import lombok.Data; |
| 5 | 5 | import lombok.experimental.Accessors; |
| 6 | 6 | import lombok.extern.slf4j.Slf4j; |
| 7 | 7 | |
| 8 | +import java.util.ArrayList; | |
| 9 | +import java.util.List; | |
| 8 | 10 | import java.util.Map; |
| 9 | 11 | import java.util.concurrent.ConcurrentHashMap; |
| 10 | 12 | |
| ... | ... | @@ -115,6 +117,24 @@ public class IotWebSocketConnectionManager { |
| 115 | 117 | } |
| 116 | 118 | |
| 117 | 119 | /** |
| 120 | + * 关闭所有连接 | |
| 121 | + */ | |
| 122 | + public void closeAll() { | |
| 123 | + // 1. 先复制再清空,避免 closeHandler 回调时并发修改 | |
| 124 | + List<ServerWebSocket> sockets = new ArrayList<>(connectionMap.keySet()); | |
| 125 | + connectionMap.clear(); | |
| 126 | + deviceSocketMap.clear(); | |
| 127 | + // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过) | |
| 128 | + for (ServerWebSocket socket : sockets) { | |
| 129 | + try { | |
| 130 | + socket.close(); | |
| 131 | + } catch (Exception ignored) { | |
| 132 | + // 连接可能已关闭,忽略异常 | |
| 133 | + } | |
| 134 | + } | |
| 135 | + } | |
| 136 | + | |
| 137 | + /** | |
| 118 | 138 | * 连接信息(包含认证信息) |
| 119 | 139 | */ |
| 120 | 140 | @Data | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java
| ... | ... | @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 9 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; | |
| 10 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | |
| 9 | 11 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; |
| 10 | 12 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; |
| 11 | 13 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; |
| ... | ... | @@ -24,6 +26,8 @@ import org.springframework.http.ResponseEntity; |
| 24 | 26 | import org.springframework.stereotype.Service; |
| 25 | 27 | import org.springframework.web.client.RestTemplate; |
| 26 | 28 | |
| 29 | +import java.util.List; | |
| 30 | + | |
| 27 | 31 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; |
| 28 | 32 | |
| 29 | 33 | /** |
| ... | ... | @@ -44,7 +48,7 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { |
| 44 | 48 | public void init() { |
| 45 | 49 | IotGatewayProperties.RpcProperties rpc = gatewayProperties.getRpc(); |
| 46 | 50 | restTemplate = new RestTemplateBuilder() |
| 47 | - .rootUri(rpc.getUrl() + "/rpc-api/iot/device") | |
| 51 | + .rootUri(rpc.getUrl()) | |
| 48 | 52 | .readTimeout(rpc.getReadTimeout()) |
| 49 | 53 | .connectTimeout(rpc.getConnectTimeout()) |
| 50 | 54 | .build(); |
| ... | ... | @@ -52,12 +56,17 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { |
| 52 | 56 | |
| 53 | 57 | @Override |
| 54 | 58 | public CommonResult<Boolean> authDevice(IotDeviceAuthReqDTO authReqDTO) { |
| 55 | - return doPost("/auth", authReqDTO, new ParameterizedTypeReference<>() { }); | |
| 59 | + return doPost("/rpc-api/iot/device/auth", authReqDTO, new ParameterizedTypeReference<>() { }); | |
| 56 | 60 | } |
| 57 | 61 | |
| 58 | 62 | @Override |
| 59 | 63 | public CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO getReqDTO) { |
| 60 | - return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { }); | |
| 64 | + return doPost("/rpc-api/iot/device/get", getReqDTO, new ParameterizedTypeReference<>() { }); | |
| 65 | + } | |
| 66 | + | |
| 67 | + @Override | |
| 68 | + public CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList(IotModbusDeviceConfigListReqDTO listReqDTO) { | |
| 69 | + return doPost("/rpc-api/iot/modbus/config-list", listReqDTO, new ParameterizedTypeReference<>() { }); | |
| 61 | 70 | } |
| 62 | 71 | |
| 63 | 72 | @Override | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.util; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 4 | 6 | |
| 5 | 7 | /** |
| 6 | 8 | * IoT 网关 MQTT 主题工具类 |
| ... | ... | @@ -44,6 +46,32 @@ public final class IotMqttTopicUtils { |
| 44 | 46 | */ |
| 45 | 47 | public static final String MQTT_ACL_PATH = "/mqtt/acl"; |
| 46 | 48 | |
| 49 | + // ========== 消息方法标准化 ========== | |
| 50 | + | |
| 51 | + /** | |
| 52 | + * 标准化设备回复消息的 method | |
| 53 | + * <p> | |
| 54 | + * MQTT 协议中,设备回复下行指令时,topic 和 method 会携带 _reply 后缀 | |
| 55 | + * (如 thing.service.invoke_reply)。平台内部统一使用基础 method(如 thing.service.invoke), | |
| 56 | + * 通过 {@link IotDeviceMessage#getCode()} 非空来识别回复消息。 | |
| 57 | + * <p> | |
| 58 | + * 此方法剥离 _reply 后缀,并确保 code 字段被设置。 | |
| 59 | + * | |
| 60 | + * @param message 设备消息 | |
| 61 | + */ | |
| 62 | + public static void normalizeReplyMethod(IotDeviceMessage message) { | |
| 63 | + String method = message.getMethod(); | |
| 64 | + if (!StrUtil.endWith(method, REPLY_TOPIC_SUFFIX)) { | |
| 65 | + return; | |
| 66 | + } | |
| 67 | + // 1. 剥离 _reply 后缀 | |
| 68 | + message.setMethod(method.substring(0, method.length() - REPLY_TOPIC_SUFFIX.length())); | |
| 69 | + // 2. 确保 code 被设置,使 isReplyMessage() 能正确识别 | |
| 70 | + if (message.getCode() == null) { | |
| 71 | + message.setCode(GlobalErrorCodeConstants.SUCCESS.getCode()); | |
| 72 | + } | |
| 73 | + } | |
| 74 | + | |
| 47 | 75 | // ========== 工具方法 ========== |
| 48 | 76 | |
| 49 | 77 | /** |
| ... | ... | @@ -101,8 +129,6 @@ public final class IotMqttTopicUtils { |
| 101 | 129 | * @param deviceName 设备名称 |
| 102 | 130 | * @return 是否允许发布 |
| 103 | 131 | */ |
| 104 | - // TODO DONE @AI:这个逻辑,是不是 mqtt 协议,也要使用???答:是通用工具方法,MQTT 协议可按需调用; | |
| 105 | - // TODO @AI:那你改下 mqtt,也调用!!! | |
| 106 | 132 | public static boolean isTopicPublishAllowed(String topic, String productKey, String deviceName) { |
| 107 | 133 | if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { |
| 108 | 134 | return false; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
| ... | ... | @@ -51,8 +51,6 @@ yudao: |
| 51 | 51 | protocol: http |
| 52 | 52 | port: 8092 |
| 53 | 53 | enabled: false |
| 54 | - http: | |
| 55 | - ssl-enabled: false | |
| 56 | 54 | # ==================================== |
| 57 | 55 | # 针对引入的 TCP 组件的配置 |
| 58 | 56 | # ==================================== |
| ... | ... | @@ -64,7 +62,6 @@ yudao: |
| 64 | 62 | tcp: |
| 65 | 63 | max-connections: 1000 |
| 66 | 64 | keep-alive-timeout-ms: 30000 |
| 67 | - ssl-enabled: false | |
| 68 | 65 | codec: |
| 69 | 66 | type: delimiter # 拆包类型:length_field / delimiter / fixed_length |
| 70 | 67 | delimiter: "\\n" # 分隔符(支持转义:\\n=换行, \\r=回车, \\t=制表符) |
| ... | ... | @@ -101,7 +98,6 @@ yudao: |
| 101 | 98 | max-message-size: 65536 # 最大消息大小(字节,默认 64KB) |
| 102 | 99 | max-frame-size: 65536 # 最大帧大小(字节,默认 64KB) |
| 103 | 100 | idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60) |
| 104 | - ssl-enabled: false # 是否启用 SSL(wss://) | |
| 105 | 101 | # ==================================== |
| 106 | 102 | # 针对引入的 CoAP 组件的配置 |
| 107 | 103 | # ==================================== |
| ... | ... | @@ -117,14 +113,13 @@ yudao: |
| 117 | 113 | # 针对引入的 MQTT 组件的配置 |
| 118 | 114 | # ==================================== |
| 119 | 115 | - id: mqtt-json |
| 120 | - enabled: true | |
| 116 | + enabled: false | |
| 121 | 117 | protocol: mqtt |
| 122 | 118 | port: 1883 |
| 123 | 119 | serialize: json |
| 124 | 120 | mqtt: |
| 125 | 121 | max-message-size: 8192 # 最大消息大小(字节) |
| 126 | 122 | connect-timeout-seconds: 60 # 连接超时时间(秒) |
| 127 | - ssl-enabled: false # 是否启用 SSL | |
| 128 | 123 | # ==================================== |
| 129 | 124 | # 针对引入的 EMQX 组件的配置 |
| 130 | 125 | # ==================================== |
| ... | ... | @@ -168,6 +163,27 @@ yudao: |
| 168 | 163 | key-store-password: "your-keystore-password" # 客户端证书库密码 |
| 169 | 164 | trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 |
| 170 | 165 | trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 |
| 166 | + # ==================================== | |
| 167 | + # 针对引入的 Modbus TCP Client 组件的配置 | |
| 168 | + # ==================================== | |
| 169 | + - id: modbus-tcp-client-1 | |
| 170 | + enabled: false | |
| 171 | + protocol: modbus_tcp_client | |
| 172 | + port: 502 | |
| 173 | + modbus-tcp-client: | |
| 174 | + config-refresh-interval: 30 # 配置刷新间隔(秒) | |
| 175 | + # ==================================== | |
| 176 | + # 针对引入的 Modbus TCP Server 组件的配置 | |
| 177 | + # ==================================== | |
| 178 | + - id: modbus-tcp-server-1 | |
| 179 | + enabled: false | |
| 180 | + protocol: modbus_tcp_server | |
| 181 | + port: 503 | |
| 182 | + modbus-tcp-server: | |
| 183 | + config-refresh-interval: 30 # 配置刷新间隔(秒) | |
| 184 | + custom-function-code: 65 # 自定义功能码(用于认证等扩展交互) | |
| 185 | + request-timeout: 5000 # Pending Request 超时时间(毫秒) | |
| 186 | + request-cleanup-interval: 10000 # Pending Request 清理间隔(毫秒) | |
| 171 | 187 | |
| 172 | 188 | --- #################### 日志相关配置 #################### |
| 173 | 189 | |
| ... | ... | @@ -189,6 +205,7 @@ logging: |
| 189 | 205 | cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG |
| 190 | 206 | cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG |
| 191 | 207 | cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG |
| 208 | + cn.iocoder.yudao.module.iot.gateway.protocol.modbus: DEBUG | |
| 192 | 209 | # 根日志级别 |
| 193 | 210 | root: INFO |
| 194 | 211 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/ModbusRtuOverTcpDemo.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus; | |
| 2 | + | |
| 3 | +import com.ghgande.j2mod.modbus.io.ModbusRTUTCPTransport; | |
| 4 | +import com.ghgande.j2mod.modbus.msg.*; | |
| 5 | +import com.ghgande.j2mod.modbus.procimg.*; | |
| 6 | +import com.ghgande.j2mod.modbus.slave.ModbusSlave; | |
| 7 | +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; | |
| 8 | + | |
| 9 | +import java.net.ServerSocket; | |
| 10 | +import java.net.Socket; | |
| 11 | + | |
| 12 | +/** | |
| 13 | + * Modbus RTU over TCP 完整 Demo | |
| 14 | + * | |
| 15 | + * 架构:Master(主站)启动 TCP Server 监听 → Slave(从站)主动 TCP 连接上来 | |
| 16 | + * 通信协议:RTU 帧格式(带 CRC)通过 TCP 传输,而非标准 MBAP 头 | |
| 17 | + * | |
| 18 | + * 流程: | |
| 19 | + * 1. Master 启动 TCP ServerSocket 监听端口 | |
| 20 | + * 2. Slave(从站模拟器)作为 TCP Client 连接到 Master | |
| 21 | + * 3. Master 通过 accept 得到的 Socket,使用 {@link ModbusRTUTCPTransport} 发送读写请求 | |
| 22 | + * | |
| 23 | + * 实现说明: | |
| 24 | + * 因为 j2mod 的 ModbusSlave 只能以 TCP Server 模式运行(监听端口等待 Master 连接), | |
| 25 | + * 不支持"Slave 作为 TCP Client 主动连接 Master"的模式。 | |
| 26 | + * 所以这里用一个 TCP 桥接(bridge)来模拟: | |
| 27 | + * - Slave 在本地内部端口启动(RTU over TCP 模式) | |
| 28 | + * - 一个桥接线程同时连接 Master Server 和 Slave 内部端口,做双向数据转发 | |
| 29 | + * - Master 视角:看到的是 Slave 主动连上来 | |
| 30 | + * | |
| 31 | + * 依赖:j2mod 3.2.1(pom.xml 中已声明) | |
| 32 | + * | |
| 33 | + * @author 芋道源码 | |
| 34 | + */ | |
| 35 | +@Deprecated // 仅技术演示,非是必须的 | |
| 36 | +public class ModbusRtuOverTcpDemo { | |
| 37 | + | |
| 38 | + /** | |
| 39 | + * Master(主站)TCP Server 监听端口 | |
| 40 | + */ | |
| 41 | + private static final int PORT = 5021; | |
| 42 | + /** | |
| 43 | + * Slave 内部端口(仅本地中转用,不对外暴露) | |
| 44 | + */ | |
| 45 | + private static final int SLAVE_INTERNAL_PORT = PORT + 100; | |
| 46 | + /** | |
| 47 | + * Modbus 从站地址 | |
| 48 | + */ | |
| 49 | + private static final int SLAVE_ID = 1; | |
| 50 | + | |
| 51 | + public static void main(String[] args) throws Exception { | |
| 52 | + // ===================== 第一步:Master 启动 TCP Server 监听 ===================== | |
| 53 | + ServerSocket serverSocket = new ServerSocket(PORT); | |
| 54 | + System.out.println("==================================================="); | |
| 55 | + System.out.println("[Master] TCP Server 已启动,监听端口: " + PORT); | |
| 56 | + System.out.println("[Master] 等待 Slave 连接..."); | |
| 57 | + System.out.println("==================================================="); | |
| 58 | + | |
| 59 | + // ===================== 第二步:后台启动 Slave,它会主动连接 Master ===================== | |
| 60 | + ModbusSlave slave = startSlaveInBackground(); | |
| 61 | + | |
| 62 | + // Master accept Slave 的连接 | |
| 63 | + Socket slaveSocket = serverSocket.accept(); | |
| 64 | + System.out.println("[Master] Slave 已连接: " + slaveSocket.getRemoteSocketAddress()); | |
| 65 | + | |
| 66 | + // ===================== 第三步:Master 通过 RTU over TCP 发送读写请求 ===================== | |
| 67 | + // 使用 ModbusRTUTCPTransport 包装 Socket(RTU 帧 = SlaveID + 功能码 + 数据 + CRC,无 MBAP 头) | |
| 68 | + ModbusRTUTCPTransport transport = new ModbusRTUTCPTransport(slaveSocket); | |
| 69 | + | |
| 70 | + try { | |
| 71 | + System.out.println("[Master] RTU over TCP 通道已建立\n"); | |
| 72 | + | |
| 73 | + // 1. 读操作演示:4 种功能码 | |
| 74 | + demoReadCoils(transport); // 功能码 01:读线圈 | |
| 75 | + demoReadDiscreteInputs(transport); // 功能码 02:读离散输入 | |
| 76 | + demoReadHoldingRegisters(transport); // 功能码 03:读保持寄存器 | |
| 77 | + demoReadInputRegisters(transport); // 功能码 04:读输入寄存器 | |
| 78 | + | |
| 79 | + // 2. 写操作演示 + 读回验证 | |
| 80 | + demoWriteCoil(transport); // 功能码 05:写单个线圈 | |
| 81 | + demoWriteRegister(transport); // 功能码 06:写单个保持寄存器 | |
| 82 | + | |
| 83 | + System.out.println("\n==================================================="); | |
| 84 | + System.out.println("所有 RTU over TCP 读写操作执行成功!"); | |
| 85 | + System.out.println("==================================================="); | |
| 86 | + } finally { | |
| 87 | + // 清理资源 | |
| 88 | + transport.close(); | |
| 89 | + slaveSocket.close(); | |
| 90 | + serverSocket.close(); | |
| 91 | + slave.close(); | |
| 92 | + System.out.println("[Master] 资源已关闭"); | |
| 93 | + } | |
| 94 | + } | |
| 95 | + | |
| 96 | + // ===================== Slave 设备模拟(作为 TCP Client 连接 Master) ===================== | |
| 97 | + | |
| 98 | + /** | |
| 99 | + * 在后台启动从站模拟器,并通过 TCP 桥接连到 Master Server | |
| 100 | + * | |
| 101 | + * @return ModbusSlave 实例(用于最后关闭资源) | |
| 102 | + */ | |
| 103 | + private static ModbusSlave startSlaveInBackground() throws Exception { | |
| 104 | + // 1. 创建进程映像,初始化寄存器数据 | |
| 105 | + SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID); | |
| 106 | + // 1.1 线圈(Coil,功能码 01/05)- 可读写,地址 0~9 | |
| 107 | + for (int i = 0; i < 10; i++) { | |
| 108 | + spi.addDigitalOut(new SimpleDigitalOut(i % 2 == 0)); | |
| 109 | + } | |
| 110 | + // 1.2 离散输入(Discrete Input,功能码 02)- 只读,地址 0~9 | |
| 111 | + for (int i = 0; i < 10; i++) { | |
| 112 | + spi.addDigitalIn(new SimpleDigitalIn(i % 3 == 0)); | |
| 113 | + } | |
| 114 | + // 1.3 保持寄存器(Holding Register,功能码 03/06/16)- 可读写,地址 0~19 | |
| 115 | + for (int i = 0; i < 20; i++) { | |
| 116 | + spi.addRegister(new SimpleRegister(i * 100)); | |
| 117 | + } | |
| 118 | + // 1.4 输入寄存器(Input Register,功能码 04)- 只读,地址 0~19 | |
| 119 | + for (int i = 0; i < 20; i++) { | |
| 120 | + spi.addInputRegister(new SimpleInputRegister(i * 10 + 1)); | |
| 121 | + } | |
| 122 | + | |
| 123 | + // 2. 启动 Slave(RTU over TCP 模式,在本地内部端口监听) | |
| 124 | + ModbusSlave slave = ModbusSlaveFactory.createTCPSlave(SLAVE_INTERNAL_PORT, 5, true); | |
| 125 | + slave.addProcessImage(SLAVE_ID, spi); | |
| 126 | + slave.open(); | |
| 127 | + System.out.println("[Slave] 从站模拟器已启动(内部端口: " + SLAVE_INTERNAL_PORT + ")"); | |
| 128 | + | |
| 129 | + // 3. 启动桥接线程:TCP Client 连接 Master Server,同时连接 Slave 内部端口,双向转发 | |
| 130 | + Thread bridgeThread = new Thread(() -> { | |
| 131 | + try { | |
| 132 | + Socket toMaster = new Socket("127.0.0.1", PORT); | |
| 133 | + Socket toSlave = new Socket("127.0.0.1", SLAVE_INTERNAL_PORT); | |
| 134 | + System.out.println("[Bridge] 已建立桥接: Master(" + PORT + ") <-> Slave(" + SLAVE_INTERNAL_PORT + ")"); | |
| 135 | + | |
| 136 | + // 双向桥接:Master ↔ Bridge ↔ Slave | |
| 137 | + Thread forward = new Thread(() -> bridge(toMaster, toSlave), "bridge-master→slave"); | |
| 138 | + Thread backward = new Thread(() -> bridge(toSlave, toMaster), "bridge-slave→master"); | |
| 139 | + forward.setDaemon(true); | |
| 140 | + backward.setDaemon(true); | |
| 141 | + forward.start(); | |
| 142 | + backward.start(); | |
| 143 | + } catch (Exception e) { | |
| 144 | + e.printStackTrace(); | |
| 145 | + } | |
| 146 | + }, "bridge-setup"); | |
| 147 | + bridgeThread.setDaemon(true); | |
| 148 | + bridgeThread.start(); | |
| 149 | + | |
| 150 | + return slave; | |
| 151 | + } | |
| 152 | + | |
| 153 | + /** | |
| 154 | + * TCP 双向桥接:从 src 读取数据,写入 dst | |
| 155 | + */ | |
| 156 | + private static void bridge(Socket src, Socket dst) { | |
| 157 | + try { | |
| 158 | + byte[] buf = new byte[1024]; | |
| 159 | + var in = src.getInputStream(); | |
| 160 | + var out = dst.getOutputStream(); | |
| 161 | + int len; | |
| 162 | + while ((len = in.read(buf)) != -1) { | |
| 163 | + out.write(buf, 0, len); | |
| 164 | + out.flush(); | |
| 165 | + } | |
| 166 | + } catch (Exception ignored) { | |
| 167 | + // 连接关闭时正常退出 | |
| 168 | + } | |
| 169 | + } | |
| 170 | + | |
| 171 | + // ===================== Master 读写操作 ===================== | |
| 172 | + | |
| 173 | + /** | |
| 174 | + * 发送请求并接收响应(通用方法) | |
| 175 | + */ | |
| 176 | + private static ModbusResponse sendRequest(ModbusRTUTCPTransport transport, ModbusRequest request) throws Exception { | |
| 177 | + request.setUnitID(SLAVE_ID); | |
| 178 | + transport.writeRequest(request); | |
| 179 | + return transport.readResponse(); | |
| 180 | + } | |
| 181 | + | |
| 182 | + /** | |
| 183 | + * 功能码 01:读线圈(Read Coils) | |
| 184 | + */ | |
| 185 | + private static void demoReadCoils(ModbusRTUTCPTransport transport) throws Exception { | |
| 186 | + ReadCoilsRequest request = new ReadCoilsRequest(0, 5); | |
| 187 | + ReadCoilsResponse response = (ReadCoilsResponse) sendRequest(transport, request); | |
| 188 | + | |
| 189 | + StringBuilder sb = new StringBuilder("[功能码 01] 读线圈(0~4): "); | |
| 190 | + for (int i = 0; i < 5; i++) { | |
| 191 | + sb.append(response.getCoilStatus(i) ? "ON" : "OFF"); | |
| 192 | + if (i < 4) { | |
| 193 | + sb.append(", "); | |
| 194 | + } | |
| 195 | + } | |
| 196 | + System.out.println(sb); | |
| 197 | + } | |
| 198 | + | |
| 199 | + /** | |
| 200 | + * 功能码 02:读离散输入(Read Discrete Inputs) | |
| 201 | + */ | |
| 202 | + private static void demoReadDiscreteInputs(ModbusRTUTCPTransport transport) throws Exception { | |
| 203 | + ReadInputDiscretesRequest request = new ReadInputDiscretesRequest(0, 5); | |
| 204 | + ReadInputDiscretesResponse response = (ReadInputDiscretesResponse) sendRequest(transport, request); | |
| 205 | + | |
| 206 | + StringBuilder sb = new StringBuilder("[功能码 02] 读离散输入(0~4): "); | |
| 207 | + for (int i = 0; i < 5; i++) { | |
| 208 | + sb.append(response.getDiscreteStatus(i) ? "ON" : "OFF"); | |
| 209 | + if (i < 4) { | |
| 210 | + sb.append(", "); | |
| 211 | + } | |
| 212 | + } | |
| 213 | + System.out.println(sb); | |
| 214 | + } | |
| 215 | + | |
| 216 | + /** | |
| 217 | + * 功能码 03:读保持寄存器(Read Holding Registers) | |
| 218 | + */ | |
| 219 | + private static void demoReadHoldingRegisters(ModbusRTUTCPTransport transport) throws Exception { | |
| 220 | + ReadMultipleRegistersRequest request = new ReadMultipleRegistersRequest(0, 5); | |
| 221 | + ReadMultipleRegistersResponse response = (ReadMultipleRegistersResponse) sendRequest(transport, request); | |
| 222 | + | |
| 223 | + StringBuilder sb = new StringBuilder("[功能码 03] 读保持寄存器(0~4): "); | |
| 224 | + for (int i = 0; i < response.getWordCount(); i++) { | |
| 225 | + sb.append(response.getRegisterValue(i)); | |
| 226 | + if (i < response.getWordCount() - 1) { | |
| 227 | + sb.append(", "); | |
| 228 | + } | |
| 229 | + } | |
| 230 | + System.out.println(sb); | |
| 231 | + } | |
| 232 | + | |
| 233 | + /** | |
| 234 | + * 功能码 04:读输入寄存器(Read Input Registers) | |
| 235 | + */ | |
| 236 | + private static void demoReadInputRegisters(ModbusRTUTCPTransport transport) throws Exception { | |
| 237 | + ReadInputRegistersRequest request = new ReadInputRegistersRequest(0, 5); | |
| 238 | + ReadInputRegistersResponse response = (ReadInputRegistersResponse) sendRequest(transport, request); | |
| 239 | + | |
| 240 | + StringBuilder sb = new StringBuilder("[功能码 04] 读输入寄存器(0~4): "); | |
| 241 | + for (int i = 0; i < response.getWordCount(); i++) { | |
| 242 | + sb.append(response.getRegisterValue(i)); | |
| 243 | + if (i < response.getWordCount() - 1) { | |
| 244 | + sb.append(", "); | |
| 245 | + } | |
| 246 | + } | |
| 247 | + System.out.println(sb); | |
| 248 | + } | |
| 249 | + | |
| 250 | + /** | |
| 251 | + * 功能码 05:写单个线圈(Write Single Coil)+ 读回验证 | |
| 252 | + */ | |
| 253 | + private static void demoWriteCoil(ModbusRTUTCPTransport transport) throws Exception { | |
| 254 | + int address = 0; | |
| 255 | + | |
| 256 | + // 1. 先读取当前值 | |
| 257 | + ReadCoilsRequest readReq = new ReadCoilsRequest(address, 1); | |
| 258 | + ReadCoilsResponse readResp = (ReadCoilsResponse) sendRequest(transport, readReq); | |
| 259 | + boolean beforeValue = readResp.getCoilStatus(0); | |
| 260 | + | |
| 261 | + // 2. 写入相反的值 | |
| 262 | + boolean writeValue = !beforeValue; | |
| 263 | + WriteCoilRequest writeReq = new WriteCoilRequest(address, writeValue); | |
| 264 | + sendRequest(transport, writeReq); | |
| 265 | + | |
| 266 | + // 3. 读回验证 | |
| 267 | + ReadCoilsResponse verifyResp = (ReadCoilsResponse) sendRequest(transport, readReq); | |
| 268 | + boolean afterValue = verifyResp.getCoilStatus(0); | |
| 269 | + | |
| 270 | + System.out.println("[功能码 05] 写线圈: 地址=" + address | |
| 271 | + + ", 写入前=" + (beforeValue ? "ON" : "OFF") | |
| 272 | + + ", 写入值=" + (writeValue ? "ON" : "OFF") | |
| 273 | + + ", 读回值=" + (afterValue ? "ON" : "OFF") | |
| 274 | + + (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败")); | |
| 275 | + } | |
| 276 | + | |
| 277 | + /** | |
| 278 | + * 功能码 06:写单个保持寄存器(Write Single Register)+ 读回验证 | |
| 279 | + */ | |
| 280 | + private static void demoWriteRegister(ModbusRTUTCPTransport transport) throws Exception { | |
| 281 | + int address = 0; | |
| 282 | + int writeValue = 12345; | |
| 283 | + | |
| 284 | + // 1. 先读取当前值 | |
| 285 | + ReadMultipleRegistersRequest readReq = new ReadMultipleRegistersRequest(address, 1); | |
| 286 | + ReadMultipleRegistersResponse readResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq); | |
| 287 | + int beforeValue = readResp.getRegisterValue(0); | |
| 288 | + | |
| 289 | + // 2. 写入新值 | |
| 290 | + WriteSingleRegisterRequest writeReq = new WriteSingleRegisterRequest(address, new SimpleRegister(writeValue)); | |
| 291 | + sendRequest(transport, writeReq); | |
| 292 | + | |
| 293 | + // 3. 读回验证 | |
| 294 | + ReadMultipleRegistersResponse verifyResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq); | |
| 295 | + int afterValue = verifyResp.getRegisterValue(0); | |
| 296 | + | |
| 297 | + System.out.println("[功能码 06] 写保持寄存器: 地址=" + address | |
| 298 | + + ", 写入前=" + beforeValue | |
| 299 | + + ", 写入值=" + writeValue | |
| 300 | + + ", 读回值=" + afterValue | |
| 301 | + + (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败")); | |
| 302 | + } | |
| 303 | + | |
| 304 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IoTModbusTcpClientIntegrationTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient; | |
| 2 | + | |
| 3 | +import com.ghgande.j2mod.modbus.procimg.*; | |
| 4 | +import com.ghgande.j2mod.modbus.slave.ModbusSlave; | |
| 5 | +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; | |
| 6 | +import lombok.extern.slf4j.Slf4j; | |
| 7 | +import org.junit.jupiter.api.Disabled; | |
| 8 | +import org.junit.jupiter.api.Test; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * Modbus TCP 从站模拟器(手动测试) | |
| 12 | + * | |
| 13 | + * <p>测试场景:模拟一个标准 Modbus TCP 从站设备,供 Modbus TCP Client 网关连接和读写数据 | |
| 14 | + * | |
| 15 | + * <p>使用步骤: | |
| 16 | + * <ol> | |
| 17 | + * <li>运行 {@link #testStartSlaveSimulator()} 启动模拟从站(默认端口 5020,从站地址 1)</li> | |
| 18 | + * <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-client 协议)</li> | |
| 19 | + * <li>确保数据库有对应的 Modbus Client 设备配置(ip=127.0.0.1, port=5020, slaveId=1)</li> | |
| 20 | + * <li>网关会自动连接模拟从站并开始轮询读取寄存器数据</li> | |
| 21 | + * <li>模拟器每 5 秒自动更新输入寄存器和保持寄存器的值,模拟传感器数据变化</li> | |
| 22 | + * </ol> | |
| 23 | + * | |
| 24 | + * <p>可用寄存器: | |
| 25 | + * <ul> | |
| 26 | + * <li>线圈 (Coil, 功能码 01/05): 地址 0-9,交替 true/false</li> | |
| 27 | + * <li>离散输入 (Discrete Input, 功能码 02): 地址 0-9,每 3 个一个 true</li> | |
| 28 | + * <li>保持寄存器 (Holding Register, 功能码 03/06/16): 地址 0-19,初始值 0,100,200,...</li> | |
| 29 | + * <li>输入寄存器 (Input Register, 功能码 04): 地址 0-19,初始值 1,11,21,...</li> | |
| 30 | + * </ul> | |
| 31 | + * | |
| 32 | + * @author 芋道源码 | |
| 33 | + */ | |
| 34 | +@Slf4j | |
| 35 | +@Disabled | |
| 36 | +public class IoTModbusTcpClientIntegrationTest { | |
| 37 | + | |
| 38 | + private static final int PORT = 5020; | |
| 39 | + private static final int SLAVE_ID = 1; | |
| 40 | + | |
| 41 | + /** | |
| 42 | + * 启动 Modbus TCP 从站模拟器 | |
| 43 | + * | |
| 44 | + * <p>模拟器会持续运行,每 5 秒更新一次寄存器数据,直到手动停止 | |
| 45 | + */ | |
| 46 | + @SuppressWarnings({"InfiniteLoopStatement", "BusyWait"}) | |
| 47 | + @Test | |
| 48 | + public void testStartSlaveSimulator() throws Exception { | |
| 49 | + // 1. 创建进程映像(Process Image),存储寄存器数据 | |
| 50 | + SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID); | |
| 51 | + | |
| 52 | + // 2.1 初始化线圈(Coil,功能码 01/05)- 离散输出,可读写 | |
| 53 | + // 地址 0-9,共 10 个线圈 | |
| 54 | + for (int i = 0; i < 10; i++) { | |
| 55 | + spi.addDigitalOut(new SimpleDigitalOut(i % 2 == 0)); // 交替 true/false | |
| 56 | + } | |
| 57 | + | |
| 58 | + // 2.2 初始化离散输入(Discrete Input,功能码 02)- 只读 | |
| 59 | + // 地址 0-9,共 10 个离散输入 | |
| 60 | + for (int i = 0; i < 10; i++) { | |
| 61 | + spi.addDigitalIn(new SimpleDigitalIn(i % 3 == 0)); // 每 3 个一个 true | |
| 62 | + } | |
| 63 | + | |
| 64 | + // 2.3 初始化保持寄存器(Holding Register,功能码 03/06/16)- 可读写 | |
| 65 | + // 地址 0-19,共 20 个寄存器 | |
| 66 | + for (int i = 0; i < 20; i++) { | |
| 67 | + spi.addRegister(new SimpleRegister(i * 100)); // 值为 0, 100, 200, ... | |
| 68 | + } | |
| 69 | + | |
| 70 | + // 2.4 初始化输入寄存器(Input Register,功能码 04)- 只读 | |
| 71 | + // 地址 0-19,共 20 个寄存器 | |
| 72 | + SimpleInputRegister[] inputRegisters = new SimpleInputRegister[20]; | |
| 73 | + for (int i = 0; i < 20; i++) { | |
| 74 | + inputRegisters[i] = new SimpleInputRegister(i * 10 + 1); // 值为 1, 11, 21, ... | |
| 75 | + spi.addInputRegister(inputRegisters[i]); | |
| 76 | + } | |
| 77 | + | |
| 78 | + // 3.1 创建 Modbus TCP 从站 | |
| 79 | + ModbusSlave slave = ModbusSlaveFactory.createTCPSlave(PORT, 5); | |
| 80 | + slave.addProcessImage(SLAVE_ID, spi); | |
| 81 | + // 3.2 启动从站 | |
| 82 | + slave.open(); | |
| 83 | + | |
| 84 | + log.info("[testStartSlaveSimulator][Modbus TCP 从站模拟器已启动, 端口: {}, 从站地址: {}]", PORT, SLAVE_ID); | |
| 85 | + log.info("[testStartSlaveSimulator][可用寄存器: 线圈(01/05) 0-9, 离散输入(02) 0-9, " + | |
| 86 | + "保持寄存器(03/06/16) 0-19, 输入寄存器(04) 0-19]"); | |
| 87 | + | |
| 88 | + // 4. 添加关闭钩子 | |
| 89 | + Runtime.getRuntime().addShutdownHook(new Thread(() -> { | |
| 90 | + log.info("[testStartSlaveSimulator][正在关闭模拟器...]"); | |
| 91 | + slave.close(); | |
| 92 | + log.info("[testStartSlaveSimulator][模拟器已关闭]"); | |
| 93 | + })); | |
| 94 | + | |
| 95 | + // 5. 保持运行,定时更新输入寄存器模拟数据变化 | |
| 96 | + int counter = 0; | |
| 97 | + while (true) { | |
| 98 | + Thread.sleep(5000); // 每 5 秒更新一次 | |
| 99 | + counter++; | |
| 100 | + | |
| 101 | + // 更新输入寄存器的值,模拟传感器数据变化 | |
| 102 | + for (int i = 0; i < 20; i++) { | |
| 103 | + int newValue = (i * 10 + 1) + counter; | |
| 104 | + inputRegisters[i].setValue(newValue); | |
| 105 | + } | |
| 106 | + | |
| 107 | + // 更新保持寄存器的第一个值 | |
| 108 | + spi.getRegister(0).setValue(counter * 100); | |
| 109 | + log.info("[testStartSlaveSimulator][数据已更新, counter={}, 保持寄存器[0]={}, 输入寄存器[0]={}]", | |
| 110 | + counter, counter * 100, 1 + counter); | |
| 111 | + } | |
| 112 | + } | |
| 113 | + | |
| 114 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerRtuIntegrationTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.HexUtil; | |
| 4 | +import cn.hutool.json.JSONObject; | |
| 5 | +import cn.hutool.json.JSONUtil; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder; | |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; | |
| 13 | +import io.vertx.core.Vertx; | |
| 14 | +import io.vertx.core.buffer.Buffer; | |
| 15 | +import io.vertx.core.net.NetClient; | |
| 16 | +import io.vertx.core.net.NetClientOptions; | |
| 17 | +import io.vertx.core.net.NetSocket; | |
| 18 | +import io.vertx.core.parsetools.RecordParser; | |
| 19 | +import lombok.extern.slf4j.Slf4j; | |
| 20 | +import org.junit.jupiter.api.AfterAll; | |
| 21 | +import org.junit.jupiter.api.BeforeAll; | |
| 22 | +import org.junit.jupiter.api.Disabled; | |
| 23 | +import org.junit.jupiter.api.Test; | |
| 24 | + | |
| 25 | +import java.util.concurrent.CompletableFuture; | |
| 26 | +import java.util.concurrent.TimeUnit; | |
| 27 | + | |
| 28 | +import static org.junit.jupiter.api.Assertions.assertEquals; | |
| 29 | + | |
| 30 | +/** | |
| 31 | + * IoT Modbus TCP Server 协议集成测试 — MODBUS_RTU 帧格式(手动测试) | |
| 32 | + * | |
| 33 | + * <p>测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_RTU(CRC16)帧格式通信 | |
| 34 | + * | |
| 35 | + * <p>使用步骤: | |
| 36 | + * <ol> | |
| 37 | + * <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-server 协议,默认端口 503)</li> | |
| 38 | + * <li>确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_rtu)</li> | |
| 39 | + * <li>运行以下测试方法: | |
| 40 | + * <ul> | |
| 41 | + * <li>{@link #testAuth()} - 自定义功能码认证</li> | |
| 42 | + * <li>{@link #testPollingResponse()} - 轮询响应</li> | |
| 43 | + * <li>{@link #testPropertySetWrite()} - 属性设置(接收写指令)</li> | |
| 44 | + * </ul> | |
| 45 | + * </li> | |
| 46 | + * </ol> | |
| 47 | + * | |
| 48 | + * @author 芋道源码 | |
| 49 | + */ | |
| 50 | +@Slf4j | |
| 51 | +@Disabled | |
| 52 | +public class IotModbusTcpServerRtuIntegrationTest { | |
| 53 | + | |
| 54 | + private static final String SERVER_HOST = "127.0.0.1"; | |
| 55 | + private static final int SERVER_PORT = 503; | |
| 56 | + private static final int TIMEOUT_MS = 5000; | |
| 57 | + | |
| 58 | + private static final int CUSTOM_FC = 65; | |
| 59 | + private static final int SLAVE_ID = 1; | |
| 60 | + | |
| 61 | + private static Vertx vertx; | |
| 62 | + private static NetClient netClient; | |
| 63 | + | |
| 64 | + // ===================== 编解码器 ===================== | |
| 65 | + | |
| 66 | + private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC); | |
| 67 | + private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC); | |
| 68 | + | |
| 69 | + // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== | |
| 70 | + | |
| 71 | + private static final String PRODUCT_KEY = "modbus_tcp_server_product_demo"; | |
| 72 | + private static final String DEVICE_NAME = "modbus_tcp_server_device_demo_rtu"; | |
| 73 | + private static final String DEVICE_SECRET = "af01c55eb8e3424bb23fc6c783936b2e"; | |
| 74 | + | |
| 75 | + @BeforeAll | |
| 76 | + static void setUp() { | |
| 77 | + vertx = Vertx.vertx(); | |
| 78 | + NetClientOptions options = new NetClientOptions() | |
| 79 | + .setConnectTimeout(TIMEOUT_MS) | |
| 80 | + .setIdleTimeout(TIMEOUT_MS); | |
| 81 | + netClient = vertx.createNetClient(options); | |
| 82 | + } | |
| 83 | + | |
| 84 | + @AfterAll | |
| 85 | + static void tearDown() { | |
| 86 | + if (netClient != null) { | |
| 87 | + netClient.close(); | |
| 88 | + } | |
| 89 | + if (vertx != null) { | |
| 90 | + vertx.close(); | |
| 91 | + } | |
| 92 | + } | |
| 93 | + | |
| 94 | + // ===================== 认证测试 ===================== | |
| 95 | + | |
| 96 | + /** | |
| 97 | + * 认证测试:发送自定义功能码 FC65 认证帧(RTU 格式),验证认证成功响应 | |
| 98 | + */ | |
| 99 | + @Test | |
| 100 | + public void testAuth() throws Exception { | |
| 101 | + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); | |
| 102 | + try { | |
| 103 | + // 1. 构造并发送认证帧 | |
| 104 | + IotModbusFrame response = authenticate(socket); | |
| 105 | + | |
| 106 | + // 2. 验证响应 | |
| 107 | + log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]", | |
| 108 | + response.getSlaveId(), response.getFunctionCode(), response.getCustomData()); | |
| 109 | + JSONObject json = JSONUtil.parseObj(response.getCustomData()); | |
| 110 | + assertEquals(0, json.getInt("code")); | |
| 111 | + log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message")); | |
| 112 | + } finally { | |
| 113 | + socket.close(); | |
| 114 | + } | |
| 115 | + } | |
| 116 | + | |
| 117 | + // ===================== 轮询响应测试 ===================== | |
| 118 | + | |
| 119 | + /** | |
| 120 | + * 轮询响应测试:认证后持续监听网关下发的读请求(RTU 格式),每次收到都自动构造读响应帧发回 | |
| 121 | + */ | |
| 122 | + @Test | |
| 123 | + public void testPollingResponse() throws Exception { | |
| 124 | + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); | |
| 125 | + try { | |
| 126 | + // 1. 先认证 | |
| 127 | + IotModbusFrame authResponse = authenticate(socket); | |
| 128 | + log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); | |
| 129 | + JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData()); | |
| 130 | + assertEquals(0, authJson.getInt("code")); | |
| 131 | + | |
| 132 | + // 2. 设置持续监听:每收到一个读请求,自动回复 | |
| 133 | + log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); | |
| 134 | + CompletableFuture<Void> done = new CompletableFuture<>(); | |
| 135 | + // 注意:使用 requestMode=true,因为设备端收到的是网关下发的读请求(非响应) | |
| 136 | + RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> { | |
| 137 | + log.info("[testPollingResponse][收到请求: slaveId={}, FC={}]", | |
| 138 | + frame.getSlaveId(), frame.getFunctionCode()); | |
| 139 | + // 解析读请求中的起始地址和数量 | |
| 140 | + byte[] pdu = frame.getPdu(); | |
| 141 | + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); | |
| 142 | + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); | |
| 143 | + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); | |
| 144 | + | |
| 145 | + // 构造读响应帧(模拟寄存器数据,RTU 格式) | |
| 146 | + int[] registerValues = new int[quantity]; | |
| 147 | + for (int i = 0; i < quantity; i++) { | |
| 148 | + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... | |
| 149 | + } | |
| 150 | + byte[] responseData = buildReadResponse(frame.getSlaveId(), | |
| 151 | + frame.getFunctionCode(), registerValues); | |
| 152 | + socket.write(Buffer.buffer(responseData)); | |
| 153 | + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); | |
| 154 | + }, true); | |
| 155 | + socket.handler(parser); | |
| 156 | + | |
| 157 | + // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求 | |
| 158 | + Thread.sleep(200000); | |
| 159 | + } finally { | |
| 160 | + socket.close(); | |
| 161 | + } | |
| 162 | + } | |
| 163 | + | |
| 164 | + // ===================== 属性设置测试 ===================== | |
| 165 | + | |
| 166 | + /** | |
| 167 | + * 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求(RTU 格式) | |
| 168 | + * <p> | |
| 169 | + * 注意:需手动在平台触发 property.set | |
| 170 | + */ | |
| 171 | + @Test | |
| 172 | + public void testPropertySetWrite() throws Exception { | |
| 173 | + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); | |
| 174 | + try { | |
| 175 | + // 1. 先认证 | |
| 176 | + IotModbusFrame authResponse = authenticate(socket); | |
| 177 | + log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData()); | |
| 178 | + | |
| 179 | + // 2. 等待网关下发写请求(需手动在平台触发 property.set) | |
| 180 | + log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]"); | |
| 181 | + IotModbusFrame writeRequest = waitForRequest(socket); | |
| 182 | + log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, pdu={}]", | |
| 183 | + writeRequest.getSlaveId(), writeRequest.getFunctionCode(), | |
| 184 | + HexUtil.encodeHexStr(writeRequest.getPdu())); | |
| 185 | + } finally { | |
| 186 | + socket.close(); | |
| 187 | + } | |
| 188 | + } | |
| 189 | + | |
| 190 | + // ===================== 辅助方法 ===================== | |
| 191 | + | |
| 192 | + /** | |
| 193 | + * 建立 TCP 连接 | |
| 194 | + */ | |
| 195 | + private CompletableFuture<NetSocket> connect() { | |
| 196 | + CompletableFuture<NetSocket> future = new CompletableFuture<>(); | |
| 197 | + netClient.connect(SERVER_PORT, SERVER_HOST) | |
| 198 | + .onSuccess(future::complete) | |
| 199 | + .onFailure(future::completeExceptionally); | |
| 200 | + return future; | |
| 201 | + } | |
| 202 | + | |
| 203 | + /** | |
| 204 | + * 执行认证并返回响应帧 | |
| 205 | + */ | |
| 206 | + private IotModbusFrame authenticate(NetSocket socket) throws Exception { | |
| 207 | + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | |
| 208 | + authInfo.setClientId(""); | |
| 209 | + byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); | |
| 210 | + return sendAndReceive(socket, authFrame); | |
| 211 | + } | |
| 212 | + | |
| 213 | + /** | |
| 214 | + * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码) | |
| 215 | + */ | |
| 216 | + private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { | |
| 217 | + CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>(); | |
| 218 | + // 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame) | |
| 219 | + RecordParser parser = FRAME_DECODER.createRecordParser( | |
| 220 | + (frame, frameFormat) -> { | |
| 221 | + try { | |
| 222 | + log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat); | |
| 223 | + responseFuture.complete(frame); | |
| 224 | + } catch (Exception e) { | |
| 225 | + responseFuture.completeExceptionally(e); | |
| 226 | + } | |
| 227 | + }); | |
| 228 | + socket.handler(parser); | |
| 229 | + | |
| 230 | + // 发送请求 | |
| 231 | + log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length); | |
| 232 | + socket.write(Buffer.buffer(frameData)); | |
| 233 | + | |
| 234 | + // 等待响应 | |
| 235 | + return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); | |
| 236 | + } | |
| 237 | + | |
| 238 | + /** | |
| 239 | + * 等待接收网关下发的请求帧(不发送,只等待接收) | |
| 240 | + */ | |
| 241 | + private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { | |
| 242 | + CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>(); | |
| 243 | + // 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame) | |
| 244 | + RecordParser parser = FRAME_DECODER.createRecordParser( | |
| 245 | + (frame, frameFormat) -> { | |
| 246 | + try { | |
| 247 | + log.info("[waitForRequest][检测到帧格式: {}]", frameFormat); | |
| 248 | + requestFuture.complete(frame); | |
| 249 | + } catch (Exception e) { | |
| 250 | + requestFuture.completeExceptionally(e); | |
| 251 | + } | |
| 252 | + }); | |
| 253 | + socket.handler(parser); | |
| 254 | + | |
| 255 | + // 等待(超时 30 秒,因为轮询间隔可能比较长) | |
| 256 | + return requestFuture.get(30000, TimeUnit.MILLISECONDS); | |
| 257 | + } | |
| 258 | + | |
| 259 | + /** | |
| 260 | + * 构造认证帧(MODBUS_RTU 格式) | |
| 261 | + * <p> | |
| 262 | + * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}} | |
| 263 | + * <p> | |
| 264 | + * RTU 帧格式:[SlaveId(1)] [FC=0x41(1)] [ByteCount(1)] [JSON(N)] [CRC16(2)] | |
| 265 | + */ | |
| 266 | + private byte[] buildAuthFrame(String clientId, String username, String password) { | |
| 267 | + JSONObject params = new JSONObject(); | |
| 268 | + params.set("clientId", clientId); | |
| 269 | + params.set("username", username); | |
| 270 | + params.set("password", password); | |
| 271 | + JSONObject json = new JSONObject(); | |
| 272 | + json.set("method", "auth"); | |
| 273 | + json.set("params", params); | |
| 274 | + return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(), | |
| 275 | + IotModbusFrameFormatEnum.MODBUS_RTU, 0); | |
| 276 | + } | |
| 277 | + | |
| 278 | + /** | |
| 279 | + * 构造 FC03/FC01-04 读响应帧(MODBUS_RTU 格式) | |
| 280 | + * <p> | |
| 281 | + * RTU 帧格式:[SlaveId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)] [CRC16(2)] | |
| 282 | + */ | |
| 283 | + private byte[] buildReadResponse(int slaveId, int functionCode, int[] registerValues) { | |
| 284 | + int byteCount = registerValues.length * 2; | |
| 285 | + // 帧长度:SlaveId(1) + FC(1) + ByteCount(1) + Data(N*2) + CRC(2) | |
| 286 | + int totalLength = 1 + 1 + 1 + byteCount + 2; | |
| 287 | + byte[] frame = new byte[totalLength]; | |
| 288 | + frame[0] = (byte) slaveId; | |
| 289 | + frame[1] = (byte) functionCode; | |
| 290 | + frame[2] = (byte) byteCount; | |
| 291 | + for (int i = 0; i < registerValues.length; i++) { | |
| 292 | + frame[3 + i * 2] = (byte) ((registerValues[i] >> 8) & 0xFF); | |
| 293 | + frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF); | |
| 294 | + } | |
| 295 | + // 计算 CRC16 | |
| 296 | + int crc = IotModbusCommonUtils.calculateCrc16(frame, totalLength - 2); | |
| 297 | + frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low | |
| 298 | + frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High | |
| 299 | + return frame; | |
| 300 | + } | |
| 301 | + | |
| 302 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerTcpIntegrationTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.HexUtil; | |
| 4 | +import cn.hutool.json.JSONObject; | |
| 5 | +import cn.hutool.json.JSONUtil; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | |
| 8 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; | |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder; | |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; | |
| 12 | +import io.vertx.core.Vertx; | |
| 13 | +import io.vertx.core.buffer.Buffer; | |
| 14 | +import io.vertx.core.net.NetClient; | |
| 15 | +import io.vertx.core.net.NetClientOptions; | |
| 16 | +import io.vertx.core.net.NetSocket; | |
| 17 | +import io.vertx.core.parsetools.RecordParser; | |
| 18 | +import lombok.extern.slf4j.Slf4j; | |
| 19 | +import org.junit.jupiter.api.AfterAll; | |
| 20 | +import org.junit.jupiter.api.BeforeAll; | |
| 21 | +import org.junit.jupiter.api.Disabled; | |
| 22 | +import org.junit.jupiter.api.Test; | |
| 23 | + | |
| 24 | +import java.nio.ByteBuffer; | |
| 25 | +import java.nio.ByteOrder; | |
| 26 | +import java.util.concurrent.CompletableFuture; | |
| 27 | +import java.util.concurrent.TimeUnit; | |
| 28 | + | |
| 29 | +import static org.junit.jupiter.api.Assertions.assertEquals; | |
| 30 | + | |
| 31 | +/** | |
| 32 | + * IoT Modbus TCP Server 协议集成测试 — MODBUS_TCP 帧格式(手动测试) | |
| 33 | + * | |
| 34 | + * <p>测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_TCP(MBAP 头)帧格式通信 | |
| 35 | + * | |
| 36 | + * <p>使用步骤: | |
| 37 | + * <ol> | |
| 38 | + * <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-server 协议,默认端口 503)</li> | |
| 39 | + * <li>确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_tcp)</li> | |
| 40 | + * <li>运行以下测试方法: | |
| 41 | + * <ul> | |
| 42 | + * <li>{@link #testAuth()} - 自定义功能码认证</li> | |
| 43 | + * <li>{@link #testPollingResponse()} - 轮询响应</li> | |
| 44 | + * <li>{@link #testPropertySetWrite()} - 属性设置(接收写指令)</li> | |
| 45 | + * </ul> | |
| 46 | + * </li> | |
| 47 | + * </ol> | |
| 48 | + * | |
| 49 | + * @author 芋道源码 | |
| 50 | + */ | |
| 51 | +@Slf4j | |
| 52 | +@Disabled | |
| 53 | +public class IotModbusTcpServerTcpIntegrationTest { | |
| 54 | + | |
| 55 | + private static final String SERVER_HOST = "127.0.0.1"; | |
| 56 | + private static final int SERVER_PORT = 503; | |
| 57 | + private static final int TIMEOUT_MS = 5000; | |
| 58 | + | |
| 59 | + private static final int CUSTOM_FC = 65; | |
| 60 | + private static final int SLAVE_ID = 1; | |
| 61 | + | |
| 62 | + private static Vertx vertx; | |
| 63 | + private static NetClient netClient; | |
| 64 | + | |
| 65 | + // ===================== 编解码器 ===================== | |
| 66 | + | |
| 67 | + private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC); | |
| 68 | + private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC); | |
| 69 | + | |
| 70 | + // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== | |
| 71 | + | |
| 72 | + private static final String PRODUCT_KEY = "modbus_tcp_server_product_demo"; | |
| 73 | + private static final String DEVICE_NAME = "modbus_tcp_server_device_demo_tcp"; | |
| 74 | + private static final String DEVICE_SECRET = "8e4adeb3d25342ab88643421d3fba3f6"; | |
| 75 | + | |
| 76 | + @BeforeAll | |
| 77 | + static void setUp() { | |
| 78 | + vertx = Vertx.vertx(); | |
| 79 | + NetClientOptions options = new NetClientOptions() | |
| 80 | + .setConnectTimeout(TIMEOUT_MS) | |
| 81 | + .setIdleTimeout(TIMEOUT_MS); | |
| 82 | + netClient = vertx.createNetClient(options); | |
| 83 | + } | |
| 84 | + | |
| 85 | + @AfterAll | |
| 86 | + static void tearDown() { | |
| 87 | + if (netClient != null) { | |
| 88 | + netClient.close(); | |
| 89 | + } | |
| 90 | + if (vertx != null) { | |
| 91 | + vertx.close(); | |
| 92 | + } | |
| 93 | + } | |
| 94 | + | |
| 95 | + // ===================== 认证测试 ===================== | |
| 96 | + | |
| 97 | + /** | |
| 98 | + * 认证测试:发送自定义功能码 FC65 认证帧,验证认证成功响应 | |
| 99 | + */ | |
| 100 | + @Test | |
| 101 | + public void testAuth() throws Exception { | |
| 102 | + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); | |
| 103 | + try { | |
| 104 | + // 1. 构造并发送认证帧 | |
| 105 | + IotModbusFrame response = authenticate(socket); | |
| 106 | + | |
| 107 | + // 2. 验证响应 | |
| 108 | + log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]", | |
| 109 | + response.getSlaveId(), response.getFunctionCode(), response.getCustomData()); | |
| 110 | + JSONObject json = JSONUtil.parseObj(response.getCustomData()); | |
| 111 | + assertEquals(0, json.getInt("code")); | |
| 112 | + log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message")); | |
| 113 | + } finally { | |
| 114 | + socket.close(); | |
| 115 | + } | |
| 116 | + } | |
| 117 | + | |
| 118 | + // ===================== 轮询响应测试 ===================== | |
| 119 | + | |
| 120 | + /** | |
| 121 | + * 轮询响应测试:认证后持续监听网关下发的读请求,每次收到都自动构造读响应帧发回 | |
| 122 | + */ | |
| 123 | + @Test | |
| 124 | + public void testPollingResponse() throws Exception { | |
| 125 | + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); | |
| 126 | + try { | |
| 127 | + // 1. 先认证 | |
| 128 | + IotModbusFrame authResponse = authenticate(socket); | |
| 129 | + log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); | |
| 130 | + JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData()); | |
| 131 | + assertEquals(0, authJson.getInt("code")); | |
| 132 | + | |
| 133 | + // 2. 设置持续监听:每收到一个读请求,自动回复 | |
| 134 | + log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); | |
| 135 | + RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> { | |
| 136 | + log.info("[testPollingResponse][收到请求: slaveId={}, FC={}, transactionId={}]", | |
| 137 | + frame.getSlaveId(), frame.getFunctionCode(), frame.getTransactionId()); | |
| 138 | + // 解析读请求中的起始地址和数量 | |
| 139 | + byte[] pdu = frame.getPdu(); | |
| 140 | + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); | |
| 141 | + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); | |
| 142 | + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); | |
| 143 | + | |
| 144 | + // 构造读响应帧(模拟寄存器数据) | |
| 145 | + int[] registerValues = new int[quantity]; | |
| 146 | + for (int i = 0; i < quantity; i++) { | |
| 147 | + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... | |
| 148 | + } | |
| 149 | + byte[] responseData = buildReadResponse(frame.getTransactionId(), | |
| 150 | + frame.getSlaveId(), frame.getFunctionCode(), registerValues); | |
| 151 | + socket.write(Buffer.buffer(responseData)); | |
| 152 | + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); | |
| 153 | + }); | |
| 154 | + socket.handler(parser); | |
| 155 | + | |
| 156 | + // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求 | |
| 157 | + Thread.sleep(200000); | |
| 158 | + } finally { | |
| 159 | + socket.close(); | |
| 160 | + } | |
| 161 | + } | |
| 162 | + | |
| 163 | + // ===================== 属性设置测试 ===================== | |
| 164 | + | |
| 165 | + /** | |
| 166 | + * 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求 | |
| 167 | + * <p> | |
| 168 | + * 注意:需手动在平台触发 property.set | |
| 169 | + */ | |
| 170 | + @Test | |
| 171 | + public void testPropertySetWrite() throws Exception { | |
| 172 | + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); | |
| 173 | + try { | |
| 174 | + // 1. 先认证 | |
| 175 | + IotModbusFrame authResponse = authenticate(socket); | |
| 176 | + log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData()); | |
| 177 | + | |
| 178 | + // 2. 等待网关下发写请求(需手动在平台触发 property.set) | |
| 179 | + log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]"); | |
| 180 | + IotModbusFrame writeRequest = waitForRequest(socket); | |
| 181 | + log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, transactionId={}, pdu={}]", | |
| 182 | + writeRequest.getSlaveId(), writeRequest.getFunctionCode(), | |
| 183 | + writeRequest.getTransactionId(), HexUtil.encodeHexStr(writeRequest.getPdu())); | |
| 184 | + } finally { | |
| 185 | + socket.close(); | |
| 186 | + } | |
| 187 | + } | |
| 188 | + | |
| 189 | + // ===================== 辅助方法 ===================== | |
| 190 | + | |
| 191 | + /** | |
| 192 | + * 建立 TCP 连接 | |
| 193 | + */ | |
| 194 | + private CompletableFuture<NetSocket> connect() { | |
| 195 | + CompletableFuture<NetSocket> future = new CompletableFuture<>(); | |
| 196 | + netClient.connect(SERVER_PORT, SERVER_HOST) | |
| 197 | + .onSuccess(future::complete) | |
| 198 | + .onFailure(future::completeExceptionally); | |
| 199 | + return future; | |
| 200 | + } | |
| 201 | + | |
| 202 | + /** | |
| 203 | + * 执行认证并返回响应帧 | |
| 204 | + */ | |
| 205 | + private IotModbusFrame authenticate(NetSocket socket) throws Exception { | |
| 206 | + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | |
| 207 | + authInfo.setClientId(""); // 特殊:考虑到 modbus 消息长度限制,默认 clientId 不发送 | |
| 208 | + byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); | |
| 209 | + return sendAndReceive(socket, authFrame); | |
| 210 | + } | |
| 211 | + | |
| 212 | + /** | |
| 213 | + * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码) | |
| 214 | + */ | |
| 215 | + private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { | |
| 216 | + CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>(); | |
| 217 | + // 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame) | |
| 218 | + RecordParser parser = FRAME_DECODER.createRecordParser( | |
| 219 | + (frame, frameFormat) -> { | |
| 220 | + try { | |
| 221 | + log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat); | |
| 222 | + responseFuture.complete(frame); | |
| 223 | + } catch (Exception e) { | |
| 224 | + responseFuture.completeExceptionally(e); | |
| 225 | + } | |
| 226 | + }); | |
| 227 | + socket.handler(parser); | |
| 228 | + | |
| 229 | + // 发送请求 | |
| 230 | + log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length); | |
| 231 | + socket.write(Buffer.buffer(frameData)); | |
| 232 | + | |
| 233 | + // 等待响应 | |
| 234 | + return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); | |
| 235 | + } | |
| 236 | + | |
| 237 | + /** | |
| 238 | + * 等待接收网关下发的请求帧(不发送,只等待接收) | |
| 239 | + */ | |
| 240 | + private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { | |
| 241 | + CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>(); | |
| 242 | + // 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame) | |
| 243 | + RecordParser parser = FRAME_DECODER.createRecordParser( | |
| 244 | + (frame, frameFormat) -> { | |
| 245 | + try { | |
| 246 | + log.info("[waitForRequest][检测到帧格式: {}]", frameFormat); | |
| 247 | + requestFuture.complete(frame); | |
| 248 | + } catch (Exception e) { | |
| 249 | + requestFuture.completeExceptionally(e); | |
| 250 | + } | |
| 251 | + }); | |
| 252 | + socket.handler(parser); | |
| 253 | + | |
| 254 | + // 等待(超时 30 秒,因为轮询间隔可能比较长) | |
| 255 | + return requestFuture.get(30000, TimeUnit.MILLISECONDS); | |
| 256 | + } | |
| 257 | + | |
| 258 | + /** | |
| 259 | + * 构造认证帧(MODBUS_TCP 格式) | |
| 260 | + * <p> | |
| 261 | + * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}} | |
| 262 | + */ | |
| 263 | + private byte[] buildAuthFrame(String clientId, String username, String password) { | |
| 264 | + JSONObject params = new JSONObject(); | |
| 265 | + params.set("clientId", clientId); | |
| 266 | + params.set("username", username); | |
| 267 | + params.set("password", password); | |
| 268 | + JSONObject json = new JSONObject(); | |
| 269 | + json.set("method", "auth"); | |
| 270 | + json.set("params", params); | |
| 271 | + return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(), | |
| 272 | + IotModbusFrameFormatEnum.MODBUS_TCP, 1); | |
| 273 | + } | |
| 274 | + | |
| 275 | + /** | |
| 276 | + * 构造 FC03/FC01-04 读响应帧(MODBUS_TCP 格式) | |
| 277 | + * <p> | |
| 278 | + * 格式:[MBAP(6)] [UnitId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)] | |
| 279 | + */ | |
| 280 | + private byte[] buildReadResponse(int transactionId, int slaveId, int functionCode, int[] registerValues) { | |
| 281 | + int byteCount = registerValues.length * 2; | |
| 282 | + // PDU: FC(1) + ByteCount(1) + Data(N*2) | |
| 283 | + int pduLength = 1 + 1 + byteCount; | |
| 284 | + // 完整帧:MBAP(6) + UnitId(1) + PDU | |
| 285 | + int totalLength = 6 + 1 + pduLength; | |
| 286 | + ByteBuffer buf = ByteBuffer.allocate(totalLength).order(ByteOrder.BIG_ENDIAN); | |
| 287 | + // MBAP Header | |
| 288 | + buf.putShort((short) transactionId); // Transaction ID | |
| 289 | + buf.putShort((short) 0); // Protocol ID | |
| 290 | + buf.putShort((short) (1 + pduLength)); // Length (UnitId + PDU) | |
| 291 | + // UnitId | |
| 292 | + buf.put((byte) slaveId); | |
| 293 | + // PDU | |
| 294 | + buf.put((byte) functionCode); | |
| 295 | + buf.put((byte) byteCount); | |
| 296 | + for (int value : registerValues) { | |
| 297 | + buf.putShort((short) value); | |
| 298 | + } | |
| 299 | + return buf.array(); | |
| 300 | + } | |
| 301 | + | |
| 302 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java
| ... | ... | @@ -239,12 +239,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { |
| 239 | 239 | log.info("[testSubscribe][连接认证成功]"); |
| 240 | 240 | |
| 241 | 241 | try { |
| 242 | - // 2. 设置消息处理器 | |
| 243 | - client.publishHandler(message -> log.info("[testSubscribe][收到消息: topic={}, payload={}]", | |
| 244 | - message.topicName(), message.payload().toString())); | |
| 242 | + // 2. 设置消息处理器:收到属性设置时,回复 _reply 消息 | |
| 243 | + client.publishHandler(message -> { | |
| 244 | + log.info("[testSubscribe][收到消息: topic={}, payload={}]", | |
| 245 | + message.topicName(), message.payload().toString()); | |
| 246 | + // 收到属性设置消息时,回复 _reply | |
| 247 | + if (message.topicName().endsWith("/thing/property/set")) { | |
| 248 | + try { | |
| 249 | + IotDeviceMessage received = SERIALIZER.deserialize(message.payload().getBytes()); | |
| 250 | + IotDeviceMessage reply = IotDeviceMessage.replyOf( | |
| 251 | + received.getRequestId(), "thing.property.set_reply", null, 0, null); | |
| 252 | + String replyTopic = String.format("/sys/%s/%s/thing/property/set_reply", PRODUCT_KEY, DEVICE_NAME); | |
| 253 | + byte[] replyPayload = SERIALIZER.serialize(reply); | |
| 254 | + client.publish(replyTopic, Buffer.buffer(replyPayload), MqttQoS.AT_LEAST_ONCE, false, false); | |
| 255 | + log.info("[testSubscribe][已回复属性设置: topic={}]", replyTopic); | |
| 256 | + } catch (Exception e) { | |
| 257 | + log.error("[testSubscribe][回复属性设置异常]", e); | |
| 258 | + } | |
| 259 | + } | |
| 260 | + }); | |
| 245 | 261 | |
| 246 | - // 3. 订阅下行主题 | |
| 247 | - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); | |
| 262 | + // 3. 订阅下行主题(属性设置 + 服务调用) | |
| 263 | + String topic = String.format("/sys/%s/%s/#", PRODUCT_KEY, DEVICE_NAME); | |
| 248 | 264 | log.info("[testSubscribe][订阅主题: {}]", topic); |
| 249 | 265 | subscribe(client, topic); |
| 250 | 266 | log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); | ... | ... |
yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java