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
Too many changes.
To preserve performance only 100 of 133 files are displayed.
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
| 1 | package cn.iocoder.yudao.module.iot.api.device; | 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 | import cn.iocoder.yudao.framework.common.enums.RpcConstants; | 6 | import cn.iocoder.yudao.framework.common.enums.RpcConstants; |
| 4 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 5 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 8 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 9 | +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | 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 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | 12 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; |
| 12 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | 13 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; |
| 13 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; | 14 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; |
| 14 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; | 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 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; | 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 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; | 21 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| 17 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; | 22 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; |
| 18 | import jakarta.annotation.Resource; | 23 | import jakarta.annotation.Resource; |
| 19 | import jakarta.annotation.security.PermitAll; | 24 | import jakarta.annotation.security.PermitAll; |
| 25 | +import org.springframework.context.annotation.Lazy; | ||
| 20 | import org.springframework.context.annotation.Primary; | 26 | import org.springframework.context.annotation.Primary; |
| 21 | import org.springframework.validation.annotation.Validated; | 27 | import org.springframework.validation.annotation.Validated; |
| 22 | import org.springframework.web.bind.annotation.PostMapping; | 28 | import org.springframework.web.bind.annotation.PostMapping; |
| 23 | import org.springframework.web.bind.annotation.RequestBody; | 29 | import org.springframework.web.bind.annotation.RequestBody; |
| 24 | import org.springframework.web.bind.annotation.RestController; | 30 | import org.springframework.web.bind.annotation.RestController; |
| 25 | 31 | ||
| 32 | +import java.util.ArrayList; | ||
| 26 | import java.util.List; | 33 | import java.util.List; |
| 34 | +import java.util.Map; | ||
| 35 | +import java.util.Set; | ||
| 27 | 36 | ||
| 28 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | 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 | * IoT 设备 API 实现类 | 41 | * IoT 设备 API 实现类 |
| @@ -41,6 +51,12 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { | @@ -41,6 +51,12 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { | ||
| 41 | private IotDeviceService deviceService; | 51 | private IotDeviceService deviceService; |
| 42 | @Resource | 52 | @Resource |
| 43 | private IotProductService productService; | 53 | private IotProductService productService; |
| 54 | + @Resource | ||
| 55 | + @Lazy // 延迟加载,解决循环依赖 | ||
| 56 | + private IotDeviceModbusConfigService modbusConfigService; | ||
| 57 | + @Resource | ||
| 58 | + @Lazy // 延迟加载,解决循环依赖 | ||
| 59 | + private IotDeviceModbusPointService modbusPointService; | ||
| 44 | 60 | ||
| 45 | @Override | 61 | @Override |
| 46 | @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth") | 62 | @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth") |
| @@ -64,6 +80,52 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { | @@ -64,6 +80,52 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { | ||
| 64 | } | 80 | } |
| 65 | 81 | ||
| 66 | @Override | 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 | @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register") | 129 | @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register") |
| 68 | @PermitAll | 130 | @PermitAll |
| 69 | public CommonResult<IotDeviceRegisterRespDTO> registerDevice(@RequestBody IotDeviceRegisterReqDTO reqDTO) { | 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,7 +2,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.PageParam; | 3 | import cn.iocoder.yudao.framework.common.pojo.PageParam; |
| 4 | import cn.iocoder.yudao.framework.common.validation.InEnum; | 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 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; | 6 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; |
| 7 | import io.swagger.v3.oas.annotations.media.Schema; | 7 | import io.swagger.v3.oas.annotations.media.Schema; |
| 8 | import lombok.Data; | 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,7 +38,7 @@ public class IotDeviceMessageRespVO { | ||
| 38 | @Schema(description = "请求编号", example = "req_123") | 38 | @Schema(description = "请求编号", example = "req_123") |
| 39 | private String requestId; | 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 | private String method; | 42 | private String method; |
| 43 | 43 | ||
| 44 | @Schema(description = "请求参数") | 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,6 +141,14 @@ public class IotProductController { | ||
| 141 | result.getData().getList()); | 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 | @GetMapping("/simple-list") | 152 | @GetMapping("/simple-list") |
| 145 | @Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项") | 153 | @Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项") |
| 146 | @Parameter(name = "deviceType", description = "设备类型", example = "1") | 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,7 +5,7 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; | ||
| 5 | import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO; | 5 | import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO; |
| 6 | import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO; | 6 | import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO; |
| 7 | import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsSummaryRespVO; | 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 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; | 9 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| 10 | import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; | 10 | import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; |
| 11 | import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService; | 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,7 +4,7 @@ import cn.iocoder.yudao.framework.mybatis.core.type.LongSetTypeHandler; | ||
| 4 | import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; | 4 | import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; |
| 5 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; | 5 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; |
| 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; | 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 | import com.baomidou.mybatisplus.annotation.KeySequence; | 8 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 9 | import com.baomidou.mybatisplus.annotation.TableField; | 9 | import com.baomidou.mybatisplus.annotation.TableField; |
| 10 | import com.baomidou.mybatisplus.annotation.TableId; | 10 | import com.baomidou.mybatisplus.annotation.TableId; |
| @@ -109,10 +109,6 @@ public class IotDeviceDO extends TenantBaseDO { | @@ -109,10 +109,6 @@ public class IotDeviceDO extends TenantBaseDO { | ||
| 109 | private LocalDateTime activeTime; | 109 | private LocalDateTime activeTime; |
| 110 | 110 | ||
| 111 | /** | 111 | /** |
| 112 | - * 设备的 IP 地址 | ||
| 113 | - */ | ||
| 114 | - private String ip; | ||
| 115 | - /** | ||
| 116 | * 固件编号 | 112 | * 固件编号 |
| 117 | * | 113 | * |
| 118 | * 关联 {@link IotOtaFirmwareDO#getId()} | 114 | * 关联 {@link IotOtaFirmwareDO#getId()} |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java
| @@ -84,7 +84,7 @@ public class IotDeviceMessageDO { | @@ -84,7 +84,7 @@ public class IotDeviceMessageDO { | ||
| 84 | * 请求方法 | 84 | * 请求方法 |
| 85 | * | 85 | * |
| 86 | * 枚举 {@link IotDeviceMessageMethodEnum} | 86 | * 枚举 {@link IotDeviceMessageMethodEnum} |
| 87 | - * 例如说:thing.property.report 属性上报 | 87 | + * 例如说:thing.property.post 属性上报 |
| 88 | */ | 88 | */ |
| 89 | private String method; | 89 | private String method; |
| 90 | /** | 90 | /** |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.dal.dataobject.device; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; | ||
| 5 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; | ||
| 6 | +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; | ||
| 7 | +import com.baomidou.mybatisplus.annotation.KeySequence; | ||
| 8 | +import com.baomidou.mybatisplus.annotation.TableId; | ||
| 9 | +import com.baomidou.mybatisplus.annotation.TableName; | ||
| 10 | +import lombok.*; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * IoT 设备 Modbus 连接配置 DO | ||
| 14 | + * | ||
| 15 | + * @author 芋道源码 | ||
| 16 | + */ | ||
| 17 | +@TableName("iot_device_modbus_config") | ||
| 18 | +@KeySequence("iot_device_modbus_config_seq") | ||
| 19 | +@Data | ||
| 20 | +@Builder | ||
| 21 | +@NoArgsConstructor | ||
| 22 | +@AllArgsConstructor | ||
| 23 | +public class IotDeviceModbusConfigDO extends TenantBaseDO { | ||
| 24 | + | ||
| 25 | + /** | ||
| 26 | + * 主键 | ||
| 27 | + */ | ||
| 28 | + @TableId | ||
| 29 | + private Long id; | ||
| 30 | + /** | ||
| 31 | + * 产品编号 | ||
| 32 | + * | ||
| 33 | + * 关联 {@link IotProductDO#getId()} | ||
| 34 | + */ | ||
| 35 | + private Long productId; | ||
| 36 | + /** | ||
| 37 | + * 设备编号 | ||
| 38 | + * | ||
| 39 | + * 关联 {@link IotDeviceDO#getId()} | ||
| 40 | + */ | ||
| 41 | + private Long deviceId; | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * Modbus 服务器 IP 地址 | ||
| 45 | + */ | ||
| 46 | + private String ip; | ||
| 47 | + /** | ||
| 48 | + * Modbus 服务器端口 | ||
| 49 | + */ | ||
| 50 | + private Integer port; | ||
| 51 | + /** | ||
| 52 | + * 从站地址 | ||
| 53 | + */ | ||
| 54 | + private Integer slaveId; | ||
| 55 | + /** | ||
| 56 | + * 连接超时时间,单位:毫秒 | ||
| 57 | + */ | ||
| 58 | + private Integer timeout; | ||
| 59 | + /** | ||
| 60 | + * 重试间隔,单位:毫秒 | ||
| 61 | + */ | ||
| 62 | + private Integer retryInterval; | ||
| 63 | + /** | ||
| 64 | + * 模式 | ||
| 65 | + * | ||
| 66 | + * @see IotModbusModeEnum | ||
| 67 | + */ | ||
| 68 | + private Integer mode; | ||
| 69 | + /** | ||
| 70 | + * 数据帧格式 | ||
| 71 | + * | ||
| 72 | + * @see IotModbusFrameFormatEnum | ||
| 73 | + */ | ||
| 74 | + private Integer frameFormat; | ||
| 75 | + /** | ||
| 76 | + * 状态 | ||
| 77 | + * | ||
| 78 | + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} | ||
| 79 | + */ | ||
| 80 | + private Integer status; | ||
| 81 | + | ||
| 82 | +} |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.dal.dataobject.device; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; | ||
| 4 | +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; | ||
| 5 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum; | ||
| 6 | +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum; | ||
| 7 | +import com.baomidou.mybatisplus.annotation.KeySequence; | ||
| 8 | +import com.baomidou.mybatisplus.annotation.TableId; | ||
| 9 | +import com.baomidou.mybatisplus.annotation.TableName; | ||
| 10 | +import lombok.*; | ||
| 11 | + | ||
| 12 | +import java.math.BigDecimal; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * IoT 设备 Modbus 点位配置 DO | ||
| 16 | + * | ||
| 17 | + * @author 芋道源码 | ||
| 18 | + */ | ||
| 19 | +@TableName("iot_device_modbus_point") | ||
| 20 | +@KeySequence("iot_device_modbus_point_seq") | ||
| 21 | +@Data | ||
| 22 | +@Builder | ||
| 23 | +@NoArgsConstructor | ||
| 24 | +@AllArgsConstructor | ||
| 25 | +public class IotDeviceModbusPointDO extends TenantBaseDO { | ||
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * 主键 | ||
| 29 | + */ | ||
| 30 | + @TableId | ||
| 31 | + private Long id; | ||
| 32 | + /** | ||
| 33 | + * 设备编号 | ||
| 34 | + * | ||
| 35 | + * 关联 {@link IotDeviceDO#getId()} | ||
| 36 | + */ | ||
| 37 | + private Long deviceId; | ||
| 38 | + /** | ||
| 39 | + * 物模型属性编号 | ||
| 40 | + * | ||
| 41 | + * 关联 {@link IotThingModelDO#getId()} | ||
| 42 | + */ | ||
| 43 | + private Long thingModelId; | ||
| 44 | + /** | ||
| 45 | + * 属性标识符 | ||
| 46 | + * | ||
| 47 | + * 冗余 {@link IotThingModelDO#getIdentifier()} | ||
| 48 | + */ | ||
| 49 | + private String identifier; | ||
| 50 | + /** | ||
| 51 | + * 属性名称 | ||
| 52 | + * | ||
| 53 | + * 冗余 {@link IotThingModelDO#getName()} | ||
| 54 | + */ | ||
| 55 | + private String name; | ||
| 56 | + | ||
| 57 | + // ========== Modbus 协议配置 ========== | ||
| 58 | + | ||
| 59 | + /** | ||
| 60 | + * Modbus 功能码 | ||
| 61 | + * | ||
| 62 | + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器) | ||
| 63 | + */ | ||
| 64 | + private Integer functionCode; | ||
| 65 | + /** | ||
| 66 | + * 寄存器起始地址 | ||
| 67 | + */ | ||
| 68 | + private Integer registerAddress; | ||
| 69 | + /** | ||
| 70 | + * 寄存器数量 | ||
| 71 | + */ | ||
| 72 | + private Integer registerCount; | ||
| 73 | + /** | ||
| 74 | + * 字节序 | ||
| 75 | + * | ||
| 76 | + * 枚举 {@link IotModbusByteOrderEnum} | ||
| 77 | + */ | ||
| 78 | + private String byteOrder; | ||
| 79 | + /** | ||
| 80 | + * 原始数据类型 | ||
| 81 | + * | ||
| 82 | + * 枚举 {@link IotModbusRawDataTypeEnum} | ||
| 83 | + */ | ||
| 84 | + private String rawDataType; | ||
| 85 | + /** | ||
| 86 | + * 缩放因子 | ||
| 87 | + */ | ||
| 88 | + private BigDecimal scale; | ||
| 89 | + /** | ||
| 90 | + * 轮询间隔(毫秒) | ||
| 91 | + */ | ||
| 92 | + private Integer pollInterval; | ||
| 93 | + /** | ||
| 94 | + * 状态 | ||
| 95 | + * | ||
| 96 | + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} | ||
| 97 | + */ | ||
| 98 | + private Integer status; | ||
| 99 | + | ||
| 100 | +} |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.dal.mysql.device; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | ||
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | ||
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; | ||
| 6 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; | ||
| 7 | +import org.apache.ibatis.annotations.Mapper; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * IoT 设备 Modbus 连接配置 Mapper | ||
| 13 | + * | ||
| 14 | + * @author 芋道源码 | ||
| 15 | + */ | ||
| 16 | +@Mapper | ||
| 17 | +public interface IotDeviceModbusConfigMapper extends BaseMapperX<IotDeviceModbusConfigDO> { | ||
| 18 | + | ||
| 19 | + default IotDeviceModbusConfigDO selectByDeviceId(Long deviceId) { | ||
| 20 | + return selectOne(IotDeviceModbusConfigDO::getDeviceId, deviceId); | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + default List<IotDeviceModbusConfigDO> selectList(IotModbusDeviceConfigListReqDTO reqDTO) { | ||
| 24 | + return selectList(new LambdaQueryWrapperX<IotDeviceModbusConfigDO>() | ||
| 25 | + .eqIfPresent(IotDeviceModbusConfigDO::getStatus, reqDTO.getStatus()) | ||
| 26 | + .eqIfPresent(IotDeviceModbusConfigDO::getMode, reqDTO.getMode()) | ||
| 27 | + .inIfPresent(IotDeviceModbusConfigDO::getDeviceId, reqDTO.getDeviceIds())); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | +} |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.dal.mysql.device; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.pojo.PageResult; | ||
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | ||
| 5 | +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | ||
| 6 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; | ||
| 7 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; | ||
| 8 | +import org.apache.ibatis.annotations.Mapper; | ||
| 9 | + | ||
| 10 | +import java.util.Collection; | ||
| 11 | +import java.util.List; | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * IoT 设备 Modbus 点位配置 Mapper | ||
| 15 | + * | ||
| 16 | + * @author 芋道源码 | ||
| 17 | + */ | ||
| 18 | +@Mapper | ||
| 19 | +public interface IotDeviceModbusPointMapper extends BaseMapperX<IotDeviceModbusPointDO> { | ||
| 20 | + | ||
| 21 | + default PageResult<IotDeviceModbusPointDO> selectPage(IotDeviceModbusPointPageReqVO reqVO) { | ||
| 22 | + return selectPage(reqVO, new LambdaQueryWrapperX<IotDeviceModbusPointDO>() | ||
| 23 | + .eqIfPresent(IotDeviceModbusPointDO::getDeviceId, reqVO.getDeviceId()) | ||
| 24 | + .likeIfPresent(IotDeviceModbusPointDO::getIdentifier, reqVO.getIdentifier()) | ||
| 25 | + .likeIfPresent(IotDeviceModbusPointDO::getName, reqVO.getName()) | ||
| 26 | + .eqIfPresent(IotDeviceModbusPointDO::getFunctionCode, reqVO.getFunctionCode()) | ||
| 27 | + .eqIfPresent(IotDeviceModbusPointDO::getStatus, reqVO.getStatus()) | ||
| 28 | + .orderByDesc(IotDeviceModbusPointDO::getId)); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + default List<IotDeviceModbusPointDO> selectListByDeviceIdsAndStatus(Collection<Long> deviceIds, Integer status) { | ||
| 32 | + return selectList(new LambdaQueryWrapperX<IotDeviceModbusPointDO>() | ||
| 33 | + .in(IotDeviceModbusPointDO::getDeviceId, deviceIds) | ||
| 34 | + .eq(IotDeviceModbusPointDO::getStatus, status)); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + default IotDeviceModbusPointDO selectByDeviceIdAndIdentifier(Long deviceId, String identifier) { | ||
| 38 | + return selectOne(IotDeviceModbusPointDO::getDeviceId, deviceId, | ||
| 39 | + IotDeviceModbusPointDO::getIdentifier, identifier); | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + default void updateByThingModelId(Long thingModelId, IotDeviceModbusPointDO updateObj) { | ||
| 43 | + update(updateObj, new LambdaQueryWrapperX<IotDeviceModbusPointDO>() | ||
| 44 | + .eq(IotDeviceModbusPointDO::getThingModelId, thingModelId)); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | +} |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java
| @@ -38,6 +38,10 @@ public interface IotProductMapper extends BaseMapperX<IotProductDO> { | @@ -38,6 +38,10 @@ public interface IotProductMapper extends BaseMapperX<IotProductDO> { | ||
| 38 | .apply("LOWER(product_key) = {0}", productKey.toLowerCase())); | 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 | default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) { | 45 | default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) { |
| 42 | return selectCount(new LambdaQueryWrapperX<IotProductDO>() | 46 | return selectCount(new LambdaQueryWrapperX<IotProductDO>() |
| 43 | .geIfPresent(IotProductDO::getCreateTime, createTime)); | 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,6 +54,14 @@ public interface ErrorCodeConstants { | ||
| 54 | ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); | 54 | ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); |
| 55 | ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); | 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 | // ========== OTA 固件相关 1-050-008-000 ========== | 65 | // ========== OTA 固件相关 1-050-008-000 ========== |
| 58 | 66 | ||
| 59 | ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); | 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,9 +19,9 @@ public enum IotDataSinkTypeEnum implements ArrayValuable<Integer> { | ||
| 19 | TCP(2, "TCP"), | 19 | TCP(2, "TCP"), |
| 20 | WEBSOCKET(3, "WebSocket"), | 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 | REDIS(21, "Redis"), | 25 | REDIS(21, "Redis"), |
| 26 | 26 | ||
| 27 | ROCKETMQ(30, "RocketMQ"), | 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,7 +4,7 @@ import cn.hutool.core.collection.CollUtil; | ||
| 4 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | 4 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 5 | import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; | 5 | import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; |
| 6 | import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; | 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 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 9 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; | 9 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 10 | import cn.iocoder.yudao.module.iot.framework.iot.config.YudaoIotProperties; | 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,7 +4,7 @@ import cn.hutool.core.collection.CollUtil; | ||
| 4 | import cn.hutool.core.util.StrUtil; | 4 | import cn.hutool.core.util.StrUtil; |
| 5 | import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; | 5 | import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; |
| 6 | import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; | 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 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; | 8 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 9 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; | 9 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; |
| 10 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; | 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,7 +3,7 @@ package cn.iocoder.yudao.module.iot.mq.consumer.device; | ||
| 3 | import cn.hutool.core.util.ObjectUtil; | 3 | import cn.hutool.core.util.ObjectUtil; |
| 4 | import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; | 4 | import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; |
| 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 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 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 7 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 8 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | 8 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; |
| 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| @@ -67,7 +67,6 @@ public class IotDeviceMessageSubscriber implements IotMessageSubscriber<IotDevic | @@ -67,7 +67,6 @@ public class IotDeviceMessageSubscriber implements IotMessageSubscriber<IotDevic | ||
| 67 | IotDeviceDO device = deviceService.validateDeviceExistsFromCache(message.getDeviceId()); | 67 | IotDeviceDO device = deviceService.validateDeviceExistsFromCache(message.getDeviceId()); |
| 68 | devicePropertyService.updateDeviceReportTimeAsync(device.getId(), LocalDateTime.now()); | 68 | devicePropertyService.updateDeviceReportTimeAsync(device.getId(), LocalDateTime.now()); |
| 69 | // 1.2 更新设备的连接 server | 69 | // 1.2 更新设备的连接 server |
| 70 | - // TODO 芋艿:HTTP 网关的上行消息,不应该更新 serverId,会覆盖掉 MQTT 等长连接的 serverId,导致下行消息无法发送。 | ||
| 71 | devicePropertyService.updateDeviceServerIdAsync(device.getId(), message.getServerId()); | 70 | devicePropertyService.updateDeviceServerIdAsync(device.getId(), message.getServerId()); |
| 72 | 71 | ||
| 73 | // 2. 未上线的设备,强制上线 | 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,7 +17,7 @@ import org.springframework.stereotype.Component; | ||
| 17 | */ | 17 | */ |
| 18 | @Component | 18 | @Component |
| 19 | @Slf4j | 19 | @Slf4j |
| 20 | -public class IotDataRuleMessageHandler implements IotMessageSubscriber<IotDeviceMessage> { | 20 | +public class IotDataRuleMessageSubscriber implements IotMessageSubscriber<IotDeviceMessage> { |
| 21 | 21 | ||
| 22 | @Resource | 22 | @Resource |
| 23 | private IotDataRuleService dataRuleService; | 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,7 +9,6 @@ import jakarta.annotation.Resource; | ||
| 9 | import lombok.extern.slf4j.Slf4j; | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | import org.springframework.stereotype.Component; | 10 | import org.springframework.stereotype.Component; |
| 11 | 11 | ||
| 12 | -// TODO @puhui999:后面重构哈 | ||
| 13 | /** | 12 | /** |
| 14 | * 针对 {@link IotDeviceMessage} 的消费者,处理规则场景 | 13 | * 针对 {@link IotDeviceMessage} 的消费者,处理规则场景 |
| 15 | * | 14 | * |
| @@ -17,7 +16,7 @@ import org.springframework.stereotype.Component; | @@ -17,7 +16,7 @@ import org.springframework.stereotype.Component; | ||
| 17 | */ | 16 | */ |
| 18 | @Component | 17 | @Component |
| 19 | @Slf4j | 18 | @Slf4j |
| 20 | -public class IotSceneRuleMessageHandler implements IotMessageSubscriber<IotDeviceMessage> { | 19 | +public class IotSceneRuleMessageSubscriber implements IotMessageSubscriber<IotDeviceMessage> { |
| 21 | 20 | ||
| 22 | @Resource | 21 | @Resource |
| 23 | private IotSceneRuleService sceneRuleService; | 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,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; | ||
| 4 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; | 4 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; |
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; | 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 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 9 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | 9 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 10 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | 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,7 +17,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; | ||
| 17 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 17 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 18 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; | 18 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; |
| 19 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 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 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 21 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 22 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | 22 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 23 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | 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,11 +3,15 @@ package cn.iocoder.yudao.module.iot.service.ota; | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | import cn.hutool.core.convert.Convert; | 4 | import cn.hutool.core.convert.Convert; |
| 5 | import cn.hutool.core.lang.Assert; | 5 | import cn.hutool.core.lang.Assert; |
| 6 | -import cn.hutool.core.map.MapUtil; | ||
| 7 | import cn.hutool.core.util.StrUtil; | 6 | import cn.hutool.core.util.StrUtil; |
| 8 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 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 | import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record.IotOtaTaskRecordPageReqVO; | 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 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 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 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; | 15 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 12 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; | 16 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; |
| 13 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; | 17 | import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; |
| @@ -133,9 +137,9 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService { | @@ -133,9 +137,9 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService { | ||
| 133 | public boolean pushOtaTaskRecord(IotOtaTaskRecordDO record, IotOtaFirmwareDO fireware, IotDeviceDO device) { | 137 | public boolean pushOtaTaskRecord(IotOtaTaskRecordDO record, IotOtaFirmwareDO fireware, IotDeviceDO device) { |
| 134 | try { | 138 | try { |
| 135 | // 1. 推送 OTA 任务记录 | 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 | deviceMessageService.sendDeviceMessage(message, device); | 143 | deviceMessageService.sendDeviceMessage(message, device); |
| 140 | 144 | ||
| 141 | // 2. 更新 OTA 升级记录状态为进行中 | 145 | // 2. 更新 OTA 升级记录状态为进行中 |
| @@ -163,17 +167,16 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService { | @@ -163,17 +167,16 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService { | ||
| 163 | 167 | ||
| 164 | @Override | 168 | @Override |
| 165 | @Transactional(rollbackFor = Exception.class) | 169 | @Transactional(rollbackFor = Exception.class) |
| 166 | - @SuppressWarnings("unchecked") | ||
| 167 | public void updateOtaRecordProgress(IotDeviceDO device, IotDeviceMessage message) { | 170 | public void updateOtaRecordProgress(IotDeviceDO device, IotDeviceMessage message) { |
| 168 | // 1.1 参数解析 | 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 | Assert.notBlank(version, "version 不能为空"); | 174 | Assert.notBlank(version, "version 不能为空"); |
| 172 | - Integer status = MapUtil.getInt(params, "status"); | 175 | + Integer status = params.getStatus(); |
| 173 | Assert.notNull(status, "status 不能为空"); | 176 | Assert.notNull(status, "status 不能为空"); |
| 174 | Assert.notNull(IotOtaTaskRecordStatusEnum.of(status), "status 状态不正确"); | 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 | Assert.notNull(progress, "progress 不能为空"); | 180 | Assert.notNull(progress, "progress 不能为空"); |
| 178 | Assert.isTrue(progress >= 0 && progress <= 100, "progress 必须在 0-100 之间"); | 181 | Assert.isTrue(progress >= 0 && progress <= 100, "progress 必须在 0-100 之间"); |
| 179 | // 1.2 查询 OTA 升级记录 | 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,6 +10,9 @@ import javax.annotation.Nullable; | ||
| 10 | import java.time.LocalDateTime; | 10 | import java.time.LocalDateTime; |
| 11 | import java.util.Collection; | 11 | import java.util.Collection; |
| 12 | import java.util.List; | 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 | * IoT 产品 Service 接口 | 18 | * IoT 产品 Service 接口 |
| @@ -122,10 +125,35 @@ public interface IotProductService { | @@ -122,10 +125,35 @@ public interface IotProductService { | ||
| 122 | Long getProductCount(@Nullable LocalDateTime createTime); | 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 | * @param ids 产品编号集合 | 148 | * @param ids 产品编号集合 |
| 128 | */ | 149 | */ |
| 129 | void validateProductsExist(Collection<Long> ids); | 150 | void validateProductsExist(Collection<Long> ids); |
| 130 | 151 | ||
| 152 | + /** | ||
| 153 | + * 同步产品的 TDengine 表结构 | ||
| 154 | + * | ||
| 155 | + * 目的:当 MySQL 和 TDengine 不同步时,强制将已发布产品的表结构同步到 TDengine 中 | ||
| 156 | + */ | ||
| 157 | + void syncProductPropertyTable(); | ||
| 158 | + | ||
| 131 | } | 159 | } |
| 132 | \ No newline at end of file | 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,8 +15,10 @@ import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; | ||
| 15 | import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; | 15 | import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; |
| 16 | import com.baomidou.dynamic.datasource.annotation.DSTransactional; | 16 | import com.baomidou.dynamic.datasource.annotation.DSTransactional; |
| 17 | import jakarta.annotation.Resource; | 17 | import jakarta.annotation.Resource; |
| 18 | +import lombok.extern.slf4j.Slf4j; | ||
| 18 | import org.springframework.cache.annotation.CacheEvict; | 19 | import org.springframework.cache.annotation.CacheEvict; |
| 19 | import org.springframework.cache.annotation.Cacheable; | 20 | import org.springframework.cache.annotation.Cacheable; |
| 21 | +import org.springframework.context.annotation.Lazy; | ||
| 20 | import org.springframework.stereotype.Service; | 22 | import org.springframework.stereotype.Service; |
| 21 | import org.springframework.validation.annotation.Validated; | 23 | import org.springframework.validation.annotation.Validated; |
| 22 | 24 | ||
| @@ -33,6 +35,7 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; | @@ -33,6 +35,7 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; | ||
| 33 | * | 35 | * |
| 34 | * @author ahh | 36 | * @author ahh |
| 35 | */ | 37 | */ |
| 38 | +@Slf4j | ||
| 36 | @Service | 39 | @Service |
| 37 | @Validated | 40 | @Validated |
| 38 | public class IotProductServiceImpl implements IotProductService { | 41 | public class IotProductServiceImpl implements IotProductService { |
| @@ -41,9 +44,10 @@ public class IotProductServiceImpl implements IotProductService { | @@ -41,9 +44,10 @@ public class IotProductServiceImpl implements IotProductService { | ||
| 41 | private IotProductMapper productMapper; | 44 | private IotProductMapper productMapper; |
| 42 | 45 | ||
| 43 | @Resource | 46 | @Resource |
| 44 | - private IotDevicePropertyService devicePropertyDataService; | ||
| 45 | - @Resource | ||
| 46 | private IotDeviceService deviceService; | 47 | private IotDeviceService deviceService; |
| 48 | + @Resource | ||
| 49 | + @Lazy // 延迟加载,避免循环依赖 | ||
| 50 | + private IotDevicePropertyService devicePropertyDataService; | ||
| 47 | 51 | ||
| 48 | @Override | 52 | @Override |
| 49 | public Long createProduct(IotProductSaveReqVO createReqVO) { | 53 | public Long createProduct(IotProductSaveReqVO createReqVO) { |
| @@ -172,6 +176,32 @@ public class IotProductServiceImpl implements IotProductService { | @@ -172,6 +176,32 @@ public class IotProductServiceImpl implements IotProductService { | ||
| 172 | } | 176 | } |
| 173 | 177 | ||
| 174 | @Override | 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 | public void validateProductsExist(Collection<Long> ids) { | 205 | public void validateProductsExist(Collection<Long> ids) { |
| 176 | if (CollUtil.isEmpty(ids)) { | 206 | if (CollUtil.isEmpty(ids)) { |
| 177 | return; | 207 | return; |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java
| @@ -14,8 +14,6 @@ import java.time.Duration; | @@ -14,8 +14,6 @@ import java.time.Duration; | ||
| 14 | 14 | ||
| 15 | // TODO @芋艿:数据库 | 15 | // TODO @芋艿:数据库 |
| 16 | // TODO @芋艿:mqtt | 16 | // TODO @芋艿:mqtt |
| 17 | -// TODO @芋艿:tcp | ||
| 18 | -// TODO @芋艿:websocket | ||
| 19 | 17 | ||
| 20 | /** | 18 | /** |
| 21 | * 可缓存的 {@link IotDataRuleAction} 抽象实现 | 19 | * 可缓存的 {@link IotDataRuleAction} 抽象实现 |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDevicePropertySetSceneRuleAction.java
| @@ -15,7 +15,6 @@ import lombok.extern.slf4j.Slf4j; | @@ -15,7 +15,6 @@ import lombok.extern.slf4j.Slf4j; | ||
| 15 | import org.springframework.stereotype.Component; | 15 | import org.springframework.stereotype.Component; |
| 16 | 16 | ||
| 17 | import java.util.List; | 17 | import java.util.List; |
| 18 | -import java.util.Map; | ||
| 19 | 18 | ||
| 20 | /** | 19 | /** |
| 21 | * IoT 设备属性设置的 {@link IotSceneRuleAction} 实现类 | 20 | * IoT 设备属性设置的 {@link IotSceneRuleAction} 实现类 |
| @@ -24,7 +23,7 @@ import java.util.Map; | @@ -24,7 +23,7 @@ import java.util.Map; | ||
| 24 | */ | 23 | */ |
| 25 | @Component | 24 | @Component |
| 26 | @Slf4j | 25 | @Slf4j |
| 27 | -public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction { | 26 | +public class IotDevicePropertySetSceneRuleAction implements IotSceneRuleAction { |
| 28 | 27 | ||
| 29 | @Resource | 28 | @Resource |
| 30 | private IotDeviceService deviceService; | 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,6 +15,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; | ||
| 15 | import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper; | 15 | import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper; |
| 16 | import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; | 16 | import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; |
| 17 | import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; | 17 | import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; |
| 18 | +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; | ||
| 18 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; | 19 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; |
| 19 | import jakarta.annotation.Resource; | 20 | import jakarta.annotation.Resource; |
| 20 | import lombok.extern.slf4j.Slf4j; | 21 | import lombok.extern.slf4j.Slf4j; |
| @@ -50,6 +51,9 @@ public class IotThingModelServiceImpl implements IotThingModelService { | @@ -50,6 +51,9 @@ public class IotThingModelServiceImpl implements IotThingModelService { | ||
| 50 | @Resource | 51 | @Resource |
| 51 | @Lazy // 延迟加载,解决循环依赖 | 52 | @Lazy // 延迟加载,解决循环依赖 |
| 52 | private IotProductService productService; | 53 | private IotProductService productService; |
| 54 | + @Resource | ||
| 55 | + @Lazy // 延迟加载,解决循环依赖 | ||
| 56 | + private IotDeviceModbusPointService deviceModbusPointService; | ||
| 53 | 57 | ||
| 54 | @Override | 58 | @Override |
| 55 | @Transactional(rollbackFor = Exception.class) | 59 | @Transactional(rollbackFor = Exception.class) |
| @@ -84,7 +88,11 @@ public class IotThingModelServiceImpl implements IotThingModelService { | @@ -84,7 +88,11 @@ public class IotThingModelServiceImpl implements IotThingModelService { | ||
| 84 | IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO); | 88 | IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO); |
| 85 | thingModelMapper.updateById(thingModel); | 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 | deleteThingModelListCache(updateReqVO.getProductId()); | 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,7 +3,7 @@ package cn.iocoder.yudao.module.iot.service.rule.scene; | ||
| 3 | import cn.hutool.core.collection.ListUtil; | 3 | import cn.hutool.core.collection.ListUtil; |
| 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 5 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | 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 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; | 7 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 8 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; | 8 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; |
| 9 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; | 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 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; | 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 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; | 5 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 6 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | 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 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; | 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 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 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 7 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; | 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 | package cn.iocoder.yudao.module.iot.core.biz; | 1 | package cn.iocoder.yudao.module.iot.core.biz; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 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 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | 5 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; |
| 9 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | 6 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; |
| 10 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; | 7 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; |
| @@ -50,4 +47,12 @@ public interface IotDeviceCommonApi { | @@ -50,4 +47,12 @@ public interface IotDeviceCommonApi { | ||
| 50 | */ | 47 | */ |
| 51 | CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO); | 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,7 +64,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> { | ||
| 64 | // ========== OTA 固件 ========== | 64 | // ========== OTA 固件 ========== |
| 65 | // 可参考:https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates | 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 | OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true), | 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,7 +25,8 @@ public enum IotProtocolTypeEnum implements ArrayValuable<String> { | ||
| 25 | MQTT("mqtt"), | 25 | MQTT("mqtt"), |
| 26 | EMQX("emqx"), | 26 | EMQX("emqx"), |
| 27 | COAP("coap"), | 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 | public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new); | 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 | package cn.iocoder.yudao.module.iot.core.mq.message; | 1 | package cn.iocoder.yudao.module.iot.core.mq.message; |
| 2 | 2 | ||
| 3 | -import cn.hutool.core.map.MapUtil; | ||
| 4 | import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | 3 | import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; |
| 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 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 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 8 | import lombok.AllArgsConstructor; | 8 | import lombok.AllArgsConstructor; |
| 9 | import lombok.Builder; | 9 | import lombok.Builder; |
| @@ -72,7 +72,7 @@ public class IotDeviceMessage { | @@ -72,7 +72,7 @@ public class IotDeviceMessage { | ||
| 72 | * 请求方法 | 72 | * 请求方法 |
| 73 | * | 73 | * |
| 74 | * 枚举 {@link IotDeviceMessageMethodEnum} | 74 | * 枚举 {@link IotDeviceMessageMethodEnum} |
| 75 | - * 例如说:thing.property.report 属性上报 | 75 | + * 例如说:thing.property.post 属性上报 |
| 76 | */ | 76 | */ |
| 77 | private String method; | 77 | private String method; |
| 78 | /** | 78 | /** |
| @@ -149,20 +149,12 @@ public class IotDeviceMessage { | @@ -149,20 +149,12 @@ public class IotDeviceMessage { | ||
| 149 | 149 | ||
| 150 | public static IotDeviceMessage buildStateUpdateOnline() { | 150 | public static IotDeviceMessage buildStateUpdateOnline() { |
| 151 | return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(), | 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 | public static IotDeviceMessage buildStateOffline() { | 155 | public static IotDeviceMessage buildStateOffline() { |
| 156 | return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(), | 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 | package cn.iocoder.yudao.module.iot.core.topic.auth; | 1 | package cn.iocoder.yudao.module.iot.core.topic.auth; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 3 | import jakarta.validation.constraints.NotEmpty; | 4 | import jakarta.validation.constraints.NotEmpty; |
| 4 | import lombok.Data; | 5 | import lombok.Data; |
| 5 | 6 | ||
| 6 | /** | 7 | /** |
| 7 | * IoT 设备动态注册 Request DTO | 8 | * IoT 设备动态注册 Request DTO |
| 8 | * <p> | 9 | * <p> |
| 9 | - * 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret | 10 | + * 用于 {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} 消息的 params 参数 |
| 11 | + * <p> | ||
| 12 | + * 直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret | ||
| 10 | * | 13 | * |
| 11 | * @author 芋道源码 | 14 | * @author 芋道源码 |
| 12 | * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | 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 | package cn.iocoder.yudao.module.iot.core.topic.auth; | 1 | package cn.iocoder.yudao.module.iot.core.topic.auth; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 3 | import lombok.AllArgsConstructor; | 4 | import lombok.AllArgsConstructor; |
| 4 | import lombok.Data; | 5 | import lombok.Data; |
| 5 | import lombok.NoArgsConstructor; | 6 | import lombok.NoArgsConstructor; |
| @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; | @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; | ||
| 7 | /** | 8 | /** |
| 8 | * IoT 设备动态注册 Response DTO | 9 | * IoT 设备动态注册 Response DTO |
| 9 | * <p> | 10 | * <p> |
| 10 | - * 用于直连设备/网关的一型一密动态注册响应 | 11 | + * 用于 {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} 响应的设备信息 |
| 11 | * | 12 | * |
| 12 | * @author 芋道源码 | 13 | * @author 芋道源码 |
| 13 | * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | 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 | package cn.iocoder.yudao.module.iot.core.topic.auth; | 1 | package cn.iocoder.yudao.module.iot.core.topic.auth; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 3 | import jakarta.validation.constraints.NotEmpty; | 4 | import jakarta.validation.constraints.NotEmpty; |
| 4 | import lombok.Data; | 5 | import lombok.Data; |
| 5 | 6 | ||
| 6 | /** | 7 | /** |
| 7 | * IoT 子设备动态注册 Request DTO | 8 | * IoT 子设备动态注册 Request DTO |
| 8 | * <p> | 9 | * <p> |
| 9 | - * 用于 thing.auth.register.sub 消息的 params 数组元素 | ||
| 10 | - * | 10 | + * 用于 {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} 消息的 params 数组元素 |
| 11 | + * <p> | ||
| 11 | * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。 | 12 | * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。 |
| 12 | * | 13 | * |
| 13 | * @author 芋道源码 | 14 | * @author 芋道源码 |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java
| 1 | package cn.iocoder.yudao.module.iot.core.topic.auth; | 1 | package cn.iocoder.yudao.module.iot.core.topic.auth; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 3 | import lombok.AllArgsConstructor; | 4 | import lombok.AllArgsConstructor; |
| 4 | import lombok.Data; | 5 | import lombok.Data; |
| 5 | import lombok.NoArgsConstructor; | 6 | import lombok.NoArgsConstructor; |
| @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; | @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; | ||
| 7 | /** | 8 | /** |
| 8 | * IoT 子设备动态注册 Response DTO | 9 | * IoT 子设备动态注册 Response DTO |
| 9 | * <p> | 10 | * <p> |
| 10 | - * 用于 thing.auth.register.sub 响应的设备信息 | 11 | + * 用于 {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} 响应的设备信息 |
| 11 | * | 12 | * |
| 12 | * @author 芋道源码 | 13 | * @author 芋道源码 |
| 13 | * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a> | 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 | package cn.iocoder.yudao.module.iot.core.topic.event; | 1 | package cn.iocoder.yudao.module.iot.core.topic.event; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 3 | import lombok.Data; | 4 | import lombok.Data; |
| 4 | 5 | ||
| 5 | /** | 6 | /** |
| 6 | * IoT 设备事件上报 Request DTO | 7 | * IoT 设备事件上报 Request DTO |
| 7 | * <p> | 8 | * <p> |
| 8 | - * 用于 thing.event.post 消息的 params 参数 | 9 | + * 用于 {@link IotDeviceMessageMethodEnum#EVENT_POST} 消息的 params 参数 |
| 9 | * | 10 | * |
| 10 | * @author 芋道源码 | 11 | * @author 芋道源码 |
| 11 | * @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-events">阿里云 - 设备上报事件</a> | 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 | package cn.iocoder.yudao.module.iot.core.topic.property; | 1 | package cn.iocoder.yudao.module.iot.core.topic.property; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | 4 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 4 | import lombok.Data; | 5 | import lombok.Data; |
| 5 | 6 | ||
| @@ -9,7 +10,7 @@ import java.util.Map; | @@ -9,7 +10,7 @@ import java.util.Map; | ||
| 9 | /** | 10 | /** |
| 10 | * IoT 设备属性批量上报 Request DTO | 11 | * IoT 设备属性批量上报 Request DTO |
| 11 | * <p> | 12 | * <p> |
| 12 | - * 用于 thing.event.property.pack.post 消息的 params 参数 | 13 | + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_PACK_POST} 消息的 params 参数 |
| 13 | * | 14 | * |
| 14 | * @author 芋道源码 | 15 | * @author 芋道源码 |
| 15 | * @see <a href="http://help.aliyun.com/zh/marketplace/gateway-reports-data-in-batches">阿里云 - 网关批量上报数据</a> | 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 | package cn.iocoder.yudao.module.iot.core.topic.property; | 1 | package cn.iocoder.yudao.module.iot.core.topic.property; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 4 | + | ||
| 3 | import java.util.HashMap; | 5 | import java.util.HashMap; |
| 4 | import java.util.Map; | 6 | import java.util.Map; |
| 5 | 7 | ||
| 6 | /** | 8 | /** |
| 7 | * IoT 设备属性上报 Request DTO | 9 | * IoT 设备属性上报 Request DTO |
| 8 | * <p> | 10 | * <p> |
| 9 | - * 用于 thing.property.post 消息的 params 参数 | 11 | + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_POST} 消息的 params 参数 |
| 10 | * <p> | 12 | * <p> |
| 11 | * 本质是一个 Map,key 为属性标识符,value 为属性值 | 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 | package cn.iocoder.yudao.module.iot.core.topic.topo; | 1 | package cn.iocoder.yudao.module.iot.core.topic.topo; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 3 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 4 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 4 | import jakarta.validation.constraints.NotEmpty; | 5 | import jakarta.validation.constraints.NotEmpty; |
| 5 | import lombok.Data; | 6 | import lombok.Data; |
| 6 | 7 | ||
| @@ -9,7 +10,7 @@ import java.util.List; | @@ -9,7 +10,7 @@ import java.util.List; | ||
| 9 | /** | 10 | /** |
| 10 | * IoT 设备拓扑添加 Request DTO | 11 | * IoT 设备拓扑添加 Request DTO |
| 11 | * <p> | 12 | * <p> |
| 12 | - * 用于 thing.topo.add 消息的 params 参数 | 13 | + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_ADD} 消息的 params 参数 |
| 13 | * | 14 | * |
| 14 | * @author 芋道源码 | 15 | * @author 芋道源码 |
| 15 | * @see <a href="http://help.aliyun.com/zh/marketplace/add-topological-relationship">阿里云 - 添加拓扑关系</a> | 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 | package cn.iocoder.yudao.module.iot.core.topic.topo; | 1 | package cn.iocoder.yudao.module.iot.core.topic.topo; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | 4 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 4 | import lombok.AllArgsConstructor; | 5 | import lombok.AllArgsConstructor; |
| 5 | import lombok.Data; | 6 | import lombok.Data; |
| @@ -10,7 +11,7 @@ import java.util.List; | @@ -10,7 +11,7 @@ import java.util.List; | ||
| 10 | /** | 11 | /** |
| 11 | * IoT 设备拓扑关系变更通知 Request DTO | 12 | * IoT 设备拓扑关系变更通知 Request DTO |
| 12 | * <p> | 13 | * <p> |
| 13 | - * 用于 thing.topo.change 下行消息的 params 参数 | 14 | + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_CHANGE} 下行消息的 params 参数 |
| 14 | * | 15 | * |
| 15 | * @author 芋道源码 | 16 | * @author 芋道源码 |
| 16 | * @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a> | 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 | package cn.iocoder.yudao.module.iot.core.topic.topo; | 1 | package cn.iocoder.yudao.module.iot.core.topic.topo; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | 4 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 4 | import jakarta.validation.Valid; | 5 | import jakarta.validation.Valid; |
| 5 | import jakarta.validation.constraints.NotEmpty; | 6 | import jakarta.validation.constraints.NotEmpty; |
| @@ -10,7 +11,7 @@ import java.util.List; | @@ -10,7 +11,7 @@ import java.util.List; | ||
| 10 | /** | 11 | /** |
| 11 | * IoT 设备拓扑删除 Request DTO | 12 | * IoT 设备拓扑删除 Request DTO |
| 12 | * <p> | 13 | * <p> |
| 13 | - * 用于 thing.topo.delete 消息的 params 参数 | 14 | + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_DELETE} 消息的 params 参数 |
| 14 | * | 15 | * |
| 15 | * @author 芋道源码 | 16 | * @author 芋道源码 |
| 16 | * @see <a href="https://help.aliyun.com/zh/marketplace/delete-a-topological-relationship">阿里云 - 删除拓扑关系</a> | 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 | package cn.iocoder.yudao.module.iot.core.topic.topo; | 1 | package cn.iocoder.yudao.module.iot.core.topic.topo; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 3 | import lombok.Data; | 4 | import lombok.Data; |
| 4 | 5 | ||
| 5 | /** | 6 | /** |
| 6 | * IoT 设备拓扑关系获取 Request DTO | 7 | * IoT 设备拓扑关系获取 Request DTO |
| 7 | * <p> | 8 | * <p> |
| 8 | - * 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展) | 9 | + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_GET} 请求的 params 参数(目前为空,预留扩展) |
| 9 | * | 10 | * |
| 10 | * @author 芋道源码 | 11 | * @author 芋道源码 |
| 11 | * @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a> | 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 | package cn.iocoder.yudao.module.iot.core.topic.topo; | 1 | package cn.iocoder.yudao.module.iot.core.topic.topo; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | 4 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 4 | import lombok.Data; | 5 | import lombok.Data; |
| 5 | 6 | ||
| @@ -8,7 +9,7 @@ import java.util.List; | @@ -8,7 +9,7 @@ import java.util.List; | ||
| 8 | /** | 9 | /** |
| 9 | * IoT 设备拓扑关系获取 Response DTO | 10 | * IoT 设备拓扑关系获取 Response DTO |
| 10 | * <p> | 11 | * <p> |
| 11 | - * 用于 thing.topo.get 响应 | 12 | + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_GET} 响应 |
| 12 | * | 13 | * |
| 13 | * @author 芋道源码 | 14 | * @author 芋道源码 |
| 14 | * @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a> | 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,6 +25,14 @@ public class IotDeviceAuthUtils { | ||
| 25 | return String.format("%s.%s", productKey, deviceName); | 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 | public static String buildUsername(String productKey, String deviceName) { | 36 | public static String buildUsername(String productKey, String deviceName) { |
| 29 | return String.format("%s&%s", deviceName, productKey); | 37 | return String.format("%s&%s", deviceName, productKey); |
| 30 | } | 38 | } |
yudao-module-iot/yudao-module-iot-gateway/pom.xml
| @@ -33,7 +33,7 @@ | @@ -33,7 +33,7 @@ | ||
| 33 | <groupId>org.apache.rocketmq</groupId> | 33 | <groupId>org.apache.rocketmq</groupId> |
| 34 | <artifactId>rocketmq-spring-boot-starter</artifactId> | 34 | <artifactId>rocketmq-spring-boot-starter</artifactId> |
| 35 | <!-- TODO @芋艿:消息队列,后续可能去掉,默认不使用 rocketmq --> | 35 | <!-- TODO @芋艿:消息队列,后续可能去掉,默认不使用 rocketmq --> |
| 36 | -<!-- <optional>true</optional> --> | 36 | + <optional>true</optional> |
| 37 | </dependency> | 37 | </dependency> |
| 38 | 38 | ||
| 39 | <!-- 工具类相关 --> | 39 | <!-- 工具类相关 --> |
| @@ -48,6 +48,13 @@ | @@ -48,6 +48,13 @@ | ||
| 48 | <artifactId>vertx-mqtt</artifactId> | 48 | <artifactId>vertx-mqtt</artifactId> |
| 49 | </dependency> | 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 | <!-- CoAP 相关 - Eclipse Californium --> | 58 | <!-- CoAP 相关 - Eclipse Californium --> |
| 52 | <dependency> | 59 | <dependency> |
| 53 | <groupId>org.eclipse.californium</groupId> | 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 +6,11 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties | ||
| 6 | import org.springframework.context.annotation.Bean; | 6 | import org.springframework.context.annotation.Bean; |
| 7 | import org.springframework.context.annotation.Configuration; | 7 | import org.springframework.context.annotation.Configuration; |
| 8 | 8 | ||
| 9 | +/** | ||
| 10 | + * IoT 网关配置类 | ||
| 11 | + * | ||
| 12 | + * @author 芋道源码 | ||
| 13 | + */ | ||
| 9 | @Configuration | 14 | @Configuration |
| 10 | @EnableConfigurationProperties(IotGatewayProperties.class) | 15 | @EnableConfigurationProperties(IotGatewayProperties.class) |
| 11 | public class IotGatewayConfiguration { | 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,6 +4,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | ||
| 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; | 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; |
| 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig; | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig; |
| 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; | 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 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; |
| @@ -166,6 +168,18 @@ public class IotGatewayProperties { | @@ -166,6 +168,18 @@ public class IotGatewayProperties { | ||
| 166 | @Valid | 168 | @Valid |
| 167 | private IotEmqxConfig emqx; | 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,7 +17,7 @@ import lombok.extern.slf4j.Slf4j; | ||
| 17 | */ | 17 | */ |
| 18 | @AllArgsConstructor | 18 | @AllArgsConstructor |
| 19 | @Slf4j | 19 | @Slf4j |
| 20 | -public abstract class IotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | 20 | +public abstract class AbstractIotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { |
| 21 | 21 | ||
| 22 | private final IotProtocol protocol; | 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,6 +7,8 @@ import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | ||
| 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; | 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 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; |
| 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; |
| 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; |
| @@ -112,6 +114,10 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -112,6 +114,10 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 112 | return createMqttProtocol(config); | 114 | return createMqttProtocol(config); |
| 113 | case EMQX: | 115 | case EMQX: |
| 114 | return createEmqxProtocol(config); | 116 | return createEmqxProtocol(config); |
| 117 | + case MODBUS_TCP_CLIENT: | ||
| 118 | + return createModbusTcpClientProtocol(config); | ||
| 119 | + case MODBUS_TCP_SERVER: | ||
| 120 | + return createModbusTcpServerProtocol(config); | ||
| 115 | default: | 121 | default: |
| 116 | throw new IllegalArgumentException(String.format( | 122 | throw new IllegalArgumentException(String.format( |
| 117 | "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); | 123 | "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); |
| @@ -188,4 +194,24 @@ public class IotProtocolManager implements SmartLifecycle { | @@ -188,4 +194,24 @@ public class IotProtocolManager implements SmartLifecycle { | ||
| 188 | return new IotEmqxProtocol(config); | 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,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 | public IotCoapProtocol(ProtocolProperties properties) { | 69 | public IotCoapProtocol(ProtocolProperties properties) { |
| 70 | IotCoapConfig coapConfig = properties.getCoap(); | 70 | IotCoapConfig coapConfig = properties.getCoap(); |
| 71 | Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); | 71 | Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); |
| 72 | this.properties = properties; | 72 | this.properties = properties; |
| 73 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | 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 | @Override | 76 | @Override |
| @@ -94,9 +90,9 @@ public class IotCoapProtocol implements IotProtocol { | @@ -94,9 +90,9 @@ public class IotCoapProtocol implements IotProtocol { | ||
| 94 | return; | 90 | return; |
| 95 | } | 91 | } |
| 96 | 92 | ||
| 97 | - IotCoapConfig coapConfig = properties.getCoap(); | ||
| 98 | try { | 93 | try { |
| 99 | // 1.1 创建 CoAP 配置 | 94 | // 1.1 创建 CoAP 配置 |
| 95 | + IotCoapConfig coapConfig = properties.getCoap(); | ||
| 100 | Configuration config = Configuration.createStandardWithoutFile(); | 96 | Configuration config = Configuration.createStandardWithoutFile(); |
| 101 | config.set(CoapConfig.COAP_PORT, properties.getPort()); | 97 | config.set(CoapConfig.COAP_PORT, properties.getPort()); |
| 102 | config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize()); | 98 | config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize()); |
| @@ -131,13 +127,12 @@ public class IotCoapProtocol implements IotProtocol { | @@ -131,13 +127,12 @@ public class IotCoapProtocol implements IotProtocol { | ||
| 131 | getId(), properties.getPort(), serverId); | 127 | getId(), properties.getPort(), serverId); |
| 132 | 128 | ||
| 133 | // 4. 启动下行消息订阅者 | 129 | // 4. 启动下行消息订阅者 |
| 130 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | ||
| 131 | + this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus); | ||
| 134 | this.downstreamSubscriber.start(); | 132 | this.downstreamSubscriber.start(); |
| 135 | } catch (Exception e) { | 133 | } catch (Exception e) { |
| 136 | log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e); | 134 | log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e); |
| 137 | - if (coapServer != null) { | ||
| 138 | - coapServer.destroy(); | ||
| 139 | - coapServer = null; | ||
| 140 | - } | 135 | + stop0(); |
| 141 | throw e; | 136 | throw e; |
| 142 | } | 137 | } |
| 143 | } | 138 | } |
| @@ -147,12 +142,19 @@ public class IotCoapProtocol implements IotProtocol { | @@ -147,12 +142,19 @@ public class IotCoapProtocol implements IotProtocol { | ||
| 147 | if (!running) { | 142 | if (!running) { |
| 148 | return; | 143 | return; |
| 149 | } | 144 | } |
| 145 | + stop0(); | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + private void stop0() { | ||
| 150 | // 1. 停止下行消息订阅者 | 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 | // 2. 关闭 CoAP 服务器 | 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,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 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 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; |
| 7 | import lombok.extern.slf4j.Slf4j; | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | ||
| @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; | @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; | ||
| 12 | * @author 芋道源码 | 12 | * @author 芋道源码 |
| 13 | */ | 13 | */ |
| 14 | @Slf4j | 14 | @Slf4j |
| 15 | -public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | 15 | +public class IotCoapDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { |
| 16 | 16 | ||
| 17 | public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) { | 17 | public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) { |
| 18 | super(protocol, messageBus); | 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,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 | public IotEmqxProtocol(ProtocolProperties properties) { | 95 | public IotEmqxProtocol(ProtocolProperties properties) { |
| 96 | Assert.notNull(properties, "协议实例配置不能为空"); | 96 | Assert.notNull(properties, "协议实例配置不能为空"); |
| @@ -101,10 +101,6 @@ public class IotEmqxProtocol implements IotProtocol { | @@ -101,10 +101,6 @@ public class IotEmqxProtocol implements IotProtocol { | ||
| 101 | "MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空"); | 101 | "MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空"); |
| 102 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | 102 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); |
| 103 | this.upstreamHandler = new IotEmqxUpstreamHandler(serverId); | 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 | @Override | 106 | @Override |
| @@ -124,7 +120,7 @@ public class IotEmqxProtocol implements IotProtocol { | @@ -124,7 +120,7 @@ public class IotEmqxProtocol implements IotProtocol { | ||
| 124 | return; | 120 | return; |
| 125 | } | 121 | } |
| 126 | 122 | ||
| 127 | - // 1.1 创建 Vertx 实例 | 123 | + // 1.1 创建 Vertx 实例 和 下行消息订阅者 |
| 128 | this.vertx = Vertx.vertx(); | 124 | this.vertx = Vertx.vertx(); |
| 129 | 125 | ||
| 130 | try { | 126 | try { |
| @@ -138,6 +134,8 @@ public class IotEmqxProtocol implements IotProtocol { | @@ -138,6 +134,8 @@ public class IotEmqxProtocol implements IotProtocol { | ||
| 138 | getId(), properties.getPort(), serverId); | 134 | getId(), properties.getPort(), serverId); |
| 139 | 135 | ||
| 140 | // 2. 启动下行消息订阅者 | 136 | // 2. 启动下行消息订阅者 |
| 137 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | ||
| 138 | + this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus); | ||
| 141 | this.downstreamSubscriber.start(); | 139 | this.downstreamSubscriber.start(); |
| 142 | } catch (Exception e) { | 140 | } catch (Exception e) { |
| 143 | log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e); | 141 | log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e); |
| @@ -157,11 +155,14 @@ public class IotEmqxProtocol implements IotProtocol { | @@ -157,11 +155,14 @@ public class IotEmqxProtocol implements IotProtocol { | ||
| 157 | 155 | ||
| 158 | private void stop0() { | 156 | private void stop0() { |
| 159 | // 1. 停止下行消息订阅者 | 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 | // 2.1 先置为 false:避免 closeHandler 触发重连 | 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,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 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 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; |
| 7 | import lombok.extern.slf4j.Slf4j; | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | ||
| @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; | @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j; | ||
| 12 | * @author 芋道源码 | 12 | * @author 芋道源码 |
| 13 | */ | 13 | */ |
| 14 | @Slf4j | 14 | @Slf4j |
| 15 | -public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | 15 | +public class IotEmqxDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { |
| 16 | 16 | ||
| 17 | private final IotEmqxDownstreamHandler downstreamHandler; | 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,6 +5,7 @@ import cn.hutool.core.util.StrUtil; | ||
| 5 | import cn.hutool.extra.spring.SpringUtil; | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | ||
| 8 | import io.vertx.mqtt.messages.MqttPublishMessage; | 9 | import io.vertx.mqtt.messages.MqttPublishMessage; |
| 9 | import lombok.extern.slf4j.Slf4j; | 10 | import lombok.extern.slf4j.Slf4j; |
| 10 | 11 | ||
| @@ -42,12 +43,14 @@ public class IotEmqxUpstreamHandler { | @@ -42,12 +43,14 @@ public class IotEmqxUpstreamHandler { | ||
| 42 | return; | 43 | return; |
| 43 | } | 44 | } |
| 44 | 45 | ||
| 45 | - // 2. 反序列化消息 | 46 | + // 2.1 反序列化消息 |
| 46 | IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); | 47 | IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); |
| 47 | if (message == null) { | 48 | if (message == null) { |
| 48 | log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); | 49 | log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); |
| 49 | return; | 50 | return; |
| 50 | } | 51 | } |
| 52 | + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀) | ||
| 53 | + IotMqttTopicUtils.normalizeReplyMethod(message); | ||
| 51 | 54 | ||
| 52 | // 3. 发送消息到队列 | 55 | // 3. 发送消息到队列 |
| 53 | deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | 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,10 +64,6 @@ public class IotHttpProtocol implements IotProtocol { | ||
| 64 | public IotHttpProtocol(ProtocolProperties properties) { | 64 | public IotHttpProtocol(ProtocolProperties properties) { |
| 65 | this.properties = properties; | 65 | this.properties = properties; |
| 66 | this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); | 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 | @Override | 69 | @Override |
| @@ -87,7 +83,7 @@ public class IotHttpProtocol implements IotProtocol { | @@ -87,7 +83,7 @@ public class IotHttpProtocol implements IotProtocol { | ||
| 87 | return; | 83 | return; |
| 88 | } | 84 | } |
| 89 | 85 | ||
| 90 | - // 1.1 创建 Vertx 实例(每个 Protocol 独立管理) | 86 | + // 1.1 创建 Vertx 实例 |
| 91 | this.vertx = Vertx.vertx(); | 87 | this.vertx = Vertx.vertx(); |
| 92 | 88 | ||
| 93 | // 1.2 创建路由 | 89 | // 1.2 创建路由 |
| @@ -123,18 +119,12 @@ public class IotHttpProtocol implements IotProtocol { | @@ -123,18 +119,12 @@ public class IotHttpProtocol implements IotProtocol { | ||
| 123 | getId(), properties.getPort(), serverId); | 119 | getId(), properties.getPort(), serverId); |
| 124 | 120 | ||
| 125 | // 2. 启动下行消息订阅者 | 121 | // 2. 启动下行消息订阅者 |
| 122 | + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | ||
| 123 | + this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); | ||
| 126 | this.downstreamSubscriber.start(); | 124 | this.downstreamSubscriber.start(); |
| 127 | } catch (Exception e) { | 125 | } catch (Exception e) { |
| 128 | log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); | 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 | throw e; | 128 | throw e; |
| 139 | } | 129 | } |
| 140 | } | 130 | } |
| @@ -144,6 +134,10 @@ public class IotHttpProtocol implements IotProtocol { | @@ -144,6 +134,10 @@ public class IotHttpProtocol implements IotProtocol { | ||
| 144 | if (!running) { | 134 | if (!running) { |
| 145 | return; | 135 | return; |
| 146 | } | 136 | } |
| 137 | + stop0(); | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + private void stop0() { | ||
| 147 | // 1. 停止下行消息订阅者 | 141 | // 1. 停止下行消息订阅者 |
| 148 | if (downstreamSubscriber != null) { | 142 | if (downstreamSubscriber != null) { |
| 149 | try { | 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,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | 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 | import lombok.extern.slf4j.Slf4j; | 7 | import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | ||
| 9 | /** | 9 | /** |
| @@ -13,7 +13,7 @@ import lombok.extern.slf4j.Slf4j; | @@ -13,7 +13,7 @@ import lombok.extern.slf4j.Slf4j; | ||
| 13 | */ | 13 | */ |
| 14 | 14 | ||
| 15 | @Slf4j | 15 | @Slf4j |
| 16 | -public class IotHttpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { | 16 | +public class IotHttpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { |
| 17 | 17 | ||
| 18 | public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) { | 18 | public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) { |
| 19 | super(protocol, messageBus); | 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 | +} |