Commit 9e2c1feae10b353cbb8bcb49a7bb5b643fe2c212
1 parent
5086e122
feat:【iot】modbus-tcp 协议接入:10% 初始化:基于 cheeky-plotting-pumpkin.md 规划
Showing
38 changed files
with
2933 additions
and
8 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
| @@ -4,11 +4,13 @@ import cn.iocoder.yudao.framework.common.enums.RpcConstants; | @@ -4,11 +4,13 @@ import cn.iocoder.yudao.framework.common.enums.RpcConstants; | ||
| 4 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 4 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 5 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 5 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 6 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | 6 | 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; | 7 | +import cn.iocoder.yudao.module.iot.core.biz.dto.*; |
| 10 | 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.device.IotDeviceModbusConfigDO; | ||
| 10 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; | ||
| 11 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; | 11 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| 12 | +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService; | ||
| 13 | +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; | ||
| 12 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; | 14 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| 13 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; | 15 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; |
| 14 | import jakarta.annotation.Resource; | 16 | import jakarta.annotation.Resource; |
| @@ -19,6 +21,9 @@ import org.springframework.web.bind.annotation.PostMapping; | @@ -19,6 +21,9 @@ import org.springframework.web.bind.annotation.PostMapping; | ||
| 19 | import org.springframework.web.bind.annotation.RequestBody; | 21 | import org.springframework.web.bind.annotation.RequestBody; |
| 20 | import org.springframework.web.bind.annotation.RestController; | 22 | import org.springframework.web.bind.annotation.RestController; |
| 21 | 23 | ||
| 24 | +import java.util.ArrayList; | ||
| 25 | +import java.util.List; | ||
| 26 | + | ||
| 22 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | 27 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 23 | 28 | ||
| 24 | /** | 29 | /** |
| @@ -35,6 +40,10 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { | @@ -35,6 +40,10 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { | ||
| 35 | private IotDeviceService deviceService; | 40 | private IotDeviceService deviceService; |
| 36 | @Resource | 41 | @Resource |
| 37 | private IotProductService productService; | 42 | private IotProductService productService; |
| 43 | + @Resource | ||
| 44 | + private IotDeviceModbusConfigService modbusConfigService; | ||
| 45 | + @Resource | ||
| 46 | + private IotDeviceModbusPointService modbusPointService; | ||
| 38 | 47 | ||
| 39 | @Override | 48 | @Override |
| 40 | @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth") | 49 | @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth") |
| @@ -57,4 +66,47 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { | @@ -57,4 +66,47 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { | ||
| 57 | })); | 66 | })); |
| 58 | } | 67 | } |
| 59 | 68 | ||
| 69 | + @Override | ||
| 70 | + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/enabled-configs") | ||
| 71 | + @PermitAll | ||
| 72 | + public CommonResult<List<IotModbusDeviceConfigRespDTO>> getEnabledModbusDeviceConfigs() { | ||
| 73 | + // 1. 获取所有启用的 Modbus 连接配置 | ||
| 74 | + List<IotDeviceModbusConfigDO> configList = modbusConfigService.getEnabledModbusConfigList(); | ||
| 75 | + if (configList.isEmpty()) { | ||
| 76 | + return success(new ArrayList<>()); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + // 2. 组装返回结果 | ||
| 80 | + List<IotModbusDeviceConfigRespDTO> result = new ArrayList<>(configList.size()); | ||
| 81 | + for (IotDeviceModbusConfigDO config : configList) { | ||
| 82 | + // 2.1 获取设备信息 | ||
| 83 | + // TODO @AI:设备需要批量读取;(先暂时不处理) | ||
| 84 | + IotDeviceDO device = deviceService.getDeviceFromCache(config.getDeviceId()); | ||
| 85 | + if (device == null) { | ||
| 86 | + continue; | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + // 2.2 获取启用的点位列表 | ||
| 90 | + // TODO @AI:看看是不是批量读取; | ||
| 91 | + List<IotDeviceModbusPointDO> pointList = modbusPointService.getEnabledModbusPointListByDeviceId(config.getDeviceId()); | ||
| 92 | + | ||
| 93 | + // 2.3 构建 DTO | ||
| 94 | + IotModbusDeviceConfigRespDTO dto = new IotModbusDeviceConfigRespDTO(); | ||
| 95 | + dto.setDeviceId(config.getDeviceId()); | ||
| 96 | + // TODO @AI:这个 productKey、deviceName 这个字段,要不要冗余到 IotDeviceModbusConfigDO 里面?(先暂时不处理) | ||
| 97 | + dto.setProductKey(device.getProductKey()); | ||
| 98 | + dto.setDeviceName(device.getDeviceName()); | ||
| 99 | + dto.setTenantId(device.getTenantId()); | ||
| 100 | + // TODO @AI:看看 dto 的转换,能不能通过 beanutils copy | ||
| 101 | + dto.setIp(config.getIp()); | ||
| 102 | + dto.setPort(config.getPort()); | ||
| 103 | + dto.setSlaveId(config.getSlaveId()); | ||
| 104 | + dto.setTimeout(config.getTimeout()); | ||
| 105 | + dto.setRetryInterval(config.getRetryInterval()); | ||
| 106 | + dto.setPoints(BeanUtils.toBean(pointList, IotModbusPointRespDTO.class)); | ||
| 107 | + result.add(dto); | ||
| 108 | + } | ||
| 109 | + return success(result); | ||
| 110 | + } | ||
| 111 | + | ||
| 60 | } | 112 | } |
| 61 | \ No newline at end of file | 113 | \ No newline at end of file |
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.pojo.PageResult; | ||
| 5 | +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | ||
| 6 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; | ||
| 7 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigRespVO; | ||
| 8 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; | ||
| 9 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; | ||
| 10 | +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService; | ||
| 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-config") | ||
| 25 | +@Validated | ||
| 26 | +public class IotDeviceModbusConfigController { | ||
| 27 | + | ||
| 28 | + @Resource | ||
| 29 | + private IotDeviceModbusConfigService modbusConfigService; | ||
| 30 | + | ||
| 31 | + // TODO @AI:create 和 update 合并成 save 接口; | ||
| 32 | + @PostMapping("/create") | ||
| 33 | + @Operation(summary = "创建设备 Modbus 连接配置") | ||
| 34 | + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:create')") | ||
| 35 | + public CommonResult<Long> createModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO createReqVO) { | ||
| 36 | + return success(modbusConfigService.createModbusConfig(createReqVO)); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + @PutMapping("/update") | ||
| 40 | + @Operation(summary = "更新设备 Modbus 连接配置") | ||
| 41 | + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:update')") | ||
| 42 | + public CommonResult<Boolean> updateModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO updateReqVO) { | ||
| 43 | + modbusConfigService.updateModbusConfig(updateReqVO); | ||
| 44 | + return success(true); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + @DeleteMapping("/delete") | ||
| 48 | + @Operation(summary = "删除设备 Modbus 连接配置") | ||
| 49 | + @Parameter(name = "id", description = "编号", required = true) | ||
| 50 | + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:delete')") | ||
| 51 | + public CommonResult<Boolean> deleteModbusConfig(@RequestParam("id") Long id) { | ||
| 52 | + modbusConfigService.deleteModbusConfig(id); | ||
| 53 | + return success(true); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + // TODO @AI:这个接口改造,支持 id 或者 deviceId;二选一查询; | ||
| 57 | + @GetMapping("/get") | ||
| 58 | + @Operation(summary = "获得设备 Modbus 连接配置") | ||
| 59 | + @Parameter(name = "id", description = "编号", required = true, example = "1024") | ||
| 60 | + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") | ||
| 61 | + public CommonResult<IotDeviceModbusConfigRespVO> getModbusConfig(@RequestParam("id") Long id) { | ||
| 62 | + IotDeviceModbusConfigDO modbusConfig = modbusConfigService.getModbusConfig(id); | ||
| 63 | + return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class)); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + // TODO @AI:合并到 getModbusConfig 接口里; | ||
| 67 | + @GetMapping("/get-by-device-id") | ||
| 68 | + @Operation(summary = "根据设备编号获得 Modbus 连接配置") | ||
| 69 | + @Parameter(name = "deviceId", description = "设备编号", required = true, example = "1024") | ||
| 70 | + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") | ||
| 71 | + public CommonResult<IotDeviceModbusConfigRespVO> getModbusConfigByDeviceId(@RequestParam("deviceId") Long deviceId) { | ||
| 72 | + IotDeviceModbusConfigDO modbusConfig = modbusConfigService.getModbusConfigByDeviceId(deviceId); | ||
| 73 | + return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class)); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + @GetMapping("/page") | ||
| 77 | + @Operation(summary = "获得设备 Modbus 连接配置分页") | ||
| 78 | + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") | ||
| 79 | + public CommonResult<PageResult<IotDeviceModbusConfigRespVO>> getModbusConfigPage(@Valid IotDeviceModbusConfigPageReqVO pageReqVO) { | ||
| 80 | + PageResult<IotDeviceModbusConfigDO> pageResult = modbusConfigService.getModbusConfigPage(pageReqVO); | ||
| 81 | + return success(BeanUtils.toBean(pageResult, IotDeviceModbusConfigRespVO.class)); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | +} |
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 java.util.List; | ||
| 21 | + | ||
| 22 | +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | ||
| 23 | + | ||
| 24 | +@Tag(name = "管理后台 - IoT 设备 Modbus 点位配置") | ||
| 25 | +@RestController | ||
| 26 | +@RequestMapping("/iot/device-modbus-point") | ||
| 27 | +@Validated | ||
| 28 | +public class IotDeviceModbusPointController { | ||
| 29 | + | ||
| 30 | + @Resource | ||
| 31 | + private IotDeviceModbusPointService modbusPointService; | ||
| 32 | + | ||
| 33 | + @PostMapping("/create") | ||
| 34 | + @Operation(summary = "创建设备 Modbus 点位配置") | ||
| 35 | + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:create')") | ||
| 36 | + public CommonResult<Long> createModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO createReqVO) { | ||
| 37 | + return success(modbusPointService.createModbusPoint(createReqVO)); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + @PutMapping("/update") | ||
| 41 | + @Operation(summary = "更新设备 Modbus 点位配置") | ||
| 42 | + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:update')") | ||
| 43 | + public CommonResult<Boolean> updateModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO updateReqVO) { | ||
| 44 | + modbusPointService.updateModbusPoint(updateReqVO); | ||
| 45 | + return success(true); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + @DeleteMapping("/delete") | ||
| 49 | + @Operation(summary = "删除设备 Modbus 点位配置") | ||
| 50 | + @Parameter(name = "id", description = "编号", required = true) | ||
| 51 | + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:delete')") | ||
| 52 | + public CommonResult<Boolean> deleteModbusPoint(@RequestParam("id") Long id) { | ||
| 53 | + modbusPointService.deleteModbusPoint(id); | ||
| 54 | + return success(true); | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + @GetMapping("/get") | ||
| 58 | + @Operation(summary = "获得设备 Modbus 点位配置") | ||
| 59 | + @Parameter(name = "id", description = "编号", required = true, example = "1024") | ||
| 60 | + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") | ||
| 61 | + public CommonResult<IotDeviceModbusPointRespVO> getModbusPoint(@RequestParam("id") Long id) { | ||
| 62 | + IotDeviceModbusPointDO modbusPoint = modbusPointService.getModbusPoint(id); | ||
| 63 | + return success(BeanUtils.toBean(modbusPoint, IotDeviceModbusPointRespVO.class)); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + @GetMapping("/page") | ||
| 67 | + @Operation(summary = "获得设备 Modbus 点位配置分页") | ||
| 68 | + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") | ||
| 69 | + public CommonResult<PageResult<IotDeviceModbusPointRespVO>> getModbusPointPage(@Valid IotDeviceModbusPointPageReqVO pageReqVO) { | ||
| 70 | + PageResult<IotDeviceModbusPointDO> pageResult = modbusPointService.getModbusPointPage(pageReqVO); | ||
| 71 | + return success(BeanUtils.toBean(pageResult, IotDeviceModbusPointRespVO.class)); | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + // TODO @AI:应该用不上这个接口?只需要 getModbusPointPage 分页 | ||
| 75 | + @GetMapping("/list-by-device-id") | ||
| 76 | + @Operation(summary = "根据设备编号获得 Modbus 点位配置列表") | ||
| 77 | + @Parameter(name = "deviceId", description = "设备编号", required = true, example = "1024") | ||
| 78 | + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") | ||
| 79 | + public CommonResult<List<IotDeviceModbusPointRespVO>> getModbusPointListByDeviceId(@RequestParam("deviceId") Long deviceId) { | ||
| 80 | + List<IotDeviceModbusPointDO> list = modbusPointService.getModbusPointListByDeviceId(deviceId); | ||
| 81 | + return success(BeanUtils.toBean(list, IotDeviceModbusPointRespVO.class)); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | +} |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigPageReqVO.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 | +// TODO @AI:不需要分页接口; | ||
| 10 | +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置分页 Request VO") | ||
| 11 | +@Data | ||
| 12 | +@EqualsAndHashCode(callSuper = true) | ||
| 13 | +@ToString(callSuper = true) | ||
| 14 | +public class IotDeviceModbusConfigPageReqVO extends PageParam { | ||
| 15 | + | ||
| 16 | + @Schema(description = "设备编号", example = "1024") | ||
| 17 | + private Long deviceId; | ||
| 18 | + | ||
| 19 | + @Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100") | ||
| 20 | + private String ip; | ||
| 21 | + | ||
| 22 | + @Schema(description = "状态", example = "0") | ||
| 23 | + private Integer status; | ||
| 24 | + | ||
| 25 | +} |
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 = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") | ||
| 37 | + private Integer status; | ||
| 38 | + | ||
| 39 | + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) | ||
| 40 | + private LocalDateTime createTime; | ||
| 41 | + | ||
| 42 | +} |
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 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 | +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置新增/修改 Request VO") | ||
| 9 | +@Data | ||
| 10 | +public class IotDeviceModbusConfigSaveReqVO { | ||
| 11 | + | ||
| 12 | + @Schema(description = "主键", example = "1") | ||
| 13 | + private Long id; | ||
| 14 | + | ||
| 15 | + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") | ||
| 16 | + @NotNull(message = "设备编号不能为空") | ||
| 17 | + private Long deviceId; | ||
| 18 | + | ||
| 19 | + @Schema(description = "Modbus 服务器 IP 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100") | ||
| 20 | + @NotEmpty(message = "Modbus 服务器 IP 地址不能为空") | ||
| 21 | + private String ip; | ||
| 22 | + | ||
| 23 | + @Schema(description = "Modbus 端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "502") | ||
| 24 | + @NotNull(message = "Modbus 端口不能为空") | ||
| 25 | + private Integer port; | ||
| 26 | + | ||
| 27 | + @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | ||
| 28 | + @NotNull(message = "从站地址不能为空") | ||
| 29 | + private Integer slaveId; | ||
| 30 | + | ||
| 31 | + @Schema(description = "连接超时时间(毫秒)", example = "3000") | ||
| 32 | + private Integer timeout; | ||
| 33 | + | ||
| 34 | + @Schema(description = "重试间隔(毫秒)", example = "1000") | ||
| 35 | + private Integer retryInterval; | ||
| 36 | + | ||
| 37 | + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") | ||
| 38 | + @NotNull(message = "状态不能为空") | ||
| 39 | + private Integer status; | ||
| 40 | + | ||
| 41 | +} |
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/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 com.baomidou.mybatisplus.annotation.KeySequence; | ||
| 5 | +import com.baomidou.mybatisplus.annotation.TableId; | ||
| 6 | +import com.baomidou.mybatisplus.annotation.TableName; | ||
| 7 | +import lombok.*; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * IoT 设备 Modbus 连接配置 DO | ||
| 11 | + * | ||
| 12 | + * @author 芋道源码 | ||
| 13 | + */ | ||
| 14 | +@TableName("iot_device_modbus_config") | ||
| 15 | +@KeySequence("iot_device_modbus_config_seq") | ||
| 16 | +@Data | ||
| 17 | +@Builder | ||
| 18 | +@NoArgsConstructor | ||
| 19 | +@AllArgsConstructor | ||
| 20 | +public class IotDeviceModbusConfigDO extends TenantBaseDO { | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * 主键 | ||
| 24 | + */ | ||
| 25 | + @TableId | ||
| 26 | + private Long id; | ||
| 27 | + // TODO @AI:增加 productId; | ||
| 28 | + /** | ||
| 29 | + * 设备编号 | ||
| 30 | + * | ||
| 31 | + * 关联 {@link IotDeviceDO#getId()} | ||
| 32 | + */ | ||
| 33 | + private Long deviceId; | ||
| 34 | + | ||
| 35 | + /** | ||
| 36 | + * Modbus 服务器 IP 地址 | ||
| 37 | + */ | ||
| 38 | + private String ip; | ||
| 39 | + /** | ||
| 40 | + * Modbus 服务器端口 | ||
| 41 | + */ | ||
| 42 | + private Integer port; | ||
| 43 | + /** | ||
| 44 | + * 从站地址 | ||
| 45 | + */ | ||
| 46 | + private Integer slaveId; | ||
| 47 | + /** | ||
| 48 | + * 连接超时时间,单位:毫秒 | ||
| 49 | + */ | ||
| 50 | + private Integer timeout; | ||
| 51 | + /** | ||
| 52 | + * 重试间隔,单位:毫秒 | ||
| 53 | + */ | ||
| 54 | + private Integer retryInterval; | ||
| 55 | + /** | ||
| 56 | + * 状态 | ||
| 57 | + * | ||
| 58 | + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} | ||
| 59 | + */ | ||
| 60 | + private Integer status; | ||
| 61 | + | ||
| 62 | +} |
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.enums.device.IotModbusByteOrderEnum; | ||
| 6 | +import cn.iocoder.yudao.module.iot.enums.device.IotModbusFunctionCodeEnum; | ||
| 7 | +import cn.iocoder.yudao.module.iot.enums.device.IotModbusRawDataTypeEnum; | ||
| 8 | +import com.baomidou.mybatisplus.annotation.KeySequence; | ||
| 9 | +import com.baomidou.mybatisplus.annotation.TableId; | ||
| 10 | +import com.baomidou.mybatisplus.annotation.TableName; | ||
| 11 | +import lombok.*; | ||
| 12 | + | ||
| 13 | +import java.math.BigDecimal; | ||
| 14 | + | ||
| 15 | +/** | ||
| 16 | + * IoT 设备 Modbus 点位配置 DO | ||
| 17 | + * | ||
| 18 | + * @author 芋道源码 | ||
| 19 | + */ | ||
| 20 | +@TableName("iot_device_modbus_point") | ||
| 21 | +@KeySequence("iot_device_modbus_point_seq") | ||
| 22 | +@Data | ||
| 23 | +@Builder | ||
| 24 | +@NoArgsConstructor | ||
| 25 | +@AllArgsConstructor | ||
| 26 | +public class IotDeviceModbusPointDO extends TenantBaseDO { | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 主键 | ||
| 30 | + */ | ||
| 31 | + @TableId | ||
| 32 | + private Long id; | ||
| 33 | + // TODO @AI:增加 productId; | ||
| 34 | + /** | ||
| 35 | + * 设备编号 | ||
| 36 | + * | ||
| 37 | + * 关联 {@link IotDeviceDO#getId()} | ||
| 38 | + */ | ||
| 39 | + private Long deviceId; | ||
| 40 | + /** | ||
| 41 | + * 物模型属性编号 | ||
| 42 | + * | ||
| 43 | + * 关联 {@link IotThingModelDO#getId()} | ||
| 44 | + */ | ||
| 45 | + private Long thingModelId; | ||
| 46 | + // TODO @AI:每次物模型的变更时,需要按需刷下 identifier、name 配置; | ||
| 47 | + /** | ||
| 48 | + * 属性标识符 | ||
| 49 | + * | ||
| 50 | + * 冗余 {@link IotThingModelDO#getIdentifier()} | ||
| 51 | + */ | ||
| 52 | + private String identifier; | ||
| 53 | + /** | ||
| 54 | + * 属性名称 | ||
| 55 | + * | ||
| 56 | + * 冗余 {@link IotThingModelDO#getName()} | ||
| 57 | + */ | ||
| 58 | + private String name; | ||
| 59 | + | ||
| 60 | + // ========== Modbus 协议配置 ========== | ||
| 61 | + | ||
| 62 | + /** | ||
| 63 | + * Modbus 功能码 | ||
| 64 | + * | ||
| 65 | + * 枚举 {@link IotModbusFunctionCodeEnum} | ||
| 66 | + */ | ||
| 67 | + private Integer functionCode; | ||
| 68 | + /** | ||
| 69 | + * 寄存器起始地址 | ||
| 70 | + */ | ||
| 71 | + private Integer registerAddress; | ||
| 72 | + /** | ||
| 73 | + * 寄存器数量 | ||
| 74 | + */ | ||
| 75 | + private Integer registerCount; | ||
| 76 | + /** | ||
| 77 | + * 字节序 | ||
| 78 | + * | ||
| 79 | + * 枚举 {@link IotModbusByteOrderEnum} | ||
| 80 | + */ | ||
| 81 | + private String byteOrder; | ||
| 82 | + /** | ||
| 83 | + * 原始数据类型 | ||
| 84 | + * | ||
| 85 | + * 枚举 {@link IotModbusRawDataTypeEnum} | ||
| 86 | + */ | ||
| 87 | + private String rawDataType; | ||
| 88 | + /** | ||
| 89 | + * 缩放因子 | ||
| 90 | + */ | ||
| 91 | + private BigDecimal scale; | ||
| 92 | + /** | ||
| 93 | + * 轮询间隔(毫秒) | ||
| 94 | + */ | ||
| 95 | + private Integer pollInterval; | ||
| 96 | + /** | ||
| 97 | + * 状态 | ||
| 98 | + * | ||
| 99 | + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} | ||
| 100 | + */ | ||
| 101 | + private Integer status; | ||
| 102 | + | ||
| 103 | +} |
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.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.IotDeviceModbusConfigPageReqVO; | ||
| 7 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; | ||
| 8 | +import org.apache.ibatis.annotations.Mapper; | ||
| 9 | + | ||
| 10 | +import java.util.List; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * IoT 设备 Modbus 连接配置 Mapper | ||
| 14 | + * | ||
| 15 | + * @author 芋道源码 | ||
| 16 | + */ | ||
| 17 | +@Mapper | ||
| 18 | +public interface IotDeviceModbusConfigMapper extends BaseMapperX<IotDeviceModbusConfigDO> { | ||
| 19 | + | ||
| 20 | + default PageResult<IotDeviceModbusConfigDO> selectPage(IotDeviceModbusConfigPageReqVO reqVO) { | ||
| 21 | + return selectPage(reqVO, new LambdaQueryWrapperX<IotDeviceModbusConfigDO>() | ||
| 22 | + .eqIfPresent(IotDeviceModbusConfigDO::getDeviceId, reqVO.getDeviceId()) | ||
| 23 | + .likeIfPresent(IotDeviceModbusConfigDO::getIp, reqVO.getIp()) | ||
| 24 | + .eqIfPresent(IotDeviceModbusConfigDO::getStatus, reqVO.getStatus()) | ||
| 25 | + .orderByDesc(IotDeviceModbusConfigDO::getId)); | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + default IotDeviceModbusConfigDO selectByDeviceId(Long deviceId) { | ||
| 29 | + return selectOne(IotDeviceModbusConfigDO::getDeviceId, deviceId); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + default List<IotDeviceModbusConfigDO> selectListByStatus(Integer status) { | ||
| 33 | + return selectList(IotDeviceModbusConfigDO::getStatus, status); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | +} |
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.List; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * IoT 设备 Modbus 点位配置 Mapper | ||
| 14 | + * | ||
| 15 | + * @author 芋道源码 | ||
| 16 | + */ | ||
| 17 | +@Mapper | ||
| 18 | +public interface IotDeviceModbusPointMapper extends BaseMapperX<IotDeviceModbusPointDO> { | ||
| 19 | + | ||
| 20 | + default PageResult<IotDeviceModbusPointDO> selectPage(IotDeviceModbusPointPageReqVO reqVO) { | ||
| 21 | + return selectPage(reqVO, new LambdaQueryWrapperX<IotDeviceModbusPointDO>() | ||
| 22 | + .eqIfPresent(IotDeviceModbusPointDO::getDeviceId, reqVO.getDeviceId()) | ||
| 23 | + .likeIfPresent(IotDeviceModbusPointDO::getIdentifier, reqVO.getIdentifier()) | ||
| 24 | + .likeIfPresent(IotDeviceModbusPointDO::getName, reqVO.getName()) | ||
| 25 | + .eqIfPresent(IotDeviceModbusPointDO::getFunctionCode, reqVO.getFunctionCode()) | ||
| 26 | + .eqIfPresent(IotDeviceModbusPointDO::getStatus, reqVO.getStatus()) | ||
| 27 | + .orderByDesc(IotDeviceModbusPointDO::getId)); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + default List<IotDeviceModbusPointDO> selectListByDeviceId(Long deviceId) { | ||
| 31 | + return selectList(IotDeviceModbusPointDO::getDeviceId, deviceId); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + // TODO @AI:是不是 selectList(f1, v1, f2, v2); | ||
| 35 | + default List<IotDeviceModbusPointDO> selectListByDeviceIdAndStatus(Long deviceId, Integer status) { | ||
| 36 | + return selectList(new LambdaQueryWrapperX<IotDeviceModbusPointDO>() | ||
| 37 | + .eq(IotDeviceModbusPointDO::getDeviceId, deviceId) | ||
| 38 | + .eq(IotDeviceModbusPointDO::getStatus, status)); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + // TODO @AI:是不是 selectOne(f1, v1, f2, v2); | ||
| 42 | + default IotDeviceModbusPointDO selectByDeviceIdAndIdentifier(Long deviceId, String identifier) { | ||
| 43 | + return selectOne(new LambdaQueryWrapperX<IotDeviceModbusPointDO>() | ||
| 44 | + .eq(IotDeviceModbusPointDO::getDeviceId, deviceId) | ||
| 45 | + .eq(IotDeviceModbusPointDO::getIdentifier, identifier)); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + // TODO @AI:是不是删除这个方法; | ||
| 49 | + default void deleteByDeviceId(Long deviceId) { | ||
| 50 | + delete(IotDeviceModbusPointDO::getDeviceId, deviceId); | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | +} |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
| @@ -41,6 +41,14 @@ public interface ErrorCodeConstants { | @@ -41,6 +41,14 @@ public interface ErrorCodeConstants { | ||
| 41 | ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); | 41 | ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); |
| 42 | ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); | 42 | ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); |
| 43 | 43 | ||
| 44 | + // ========== 设备 Modbus 配置 1-050-006-000 ========== | ||
| 45 | + ErrorCode DEVICE_MODBUS_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "设备 Modbus 连接配置不存在"); | ||
| 46 | + ErrorCode DEVICE_MODBUS_CONFIG_EXISTS = new ErrorCode(1_050_006_001, "设备 Modbus 连接配置已存在"); | ||
| 47 | + | ||
| 48 | + // ========== 设备 Modbus 点位 1-050-007-000 ========== | ||
| 49 | + ErrorCode DEVICE_MODBUS_POINT_NOT_EXISTS = new ErrorCode(1_050_007_000, "设备 Modbus 点位配置不存在"); | ||
| 50 | + ErrorCode DEVICE_MODBUS_POINT_EXISTS = new ErrorCode(1_050_007_001, "设备 Modbus 点位配置已存在"); | ||
| 51 | + | ||
| 44 | // ========== OTA 固件相关 1-050-008-000 ========== | 52 | // ========== OTA 固件相关 1-050-008-000 ========== |
| 45 | 53 | ||
| 46 | ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); | 54 | 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/device/IotModbusByteOrderEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.enums.device; | ||
| 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 | + // TODO @AI:如果不需要,可以删除掉这个方法; | ||
| 48 | + /** | ||
| 49 | + * 根据字节序获取枚举 | ||
| 50 | + * | ||
| 51 | + * @param order 字节序 | ||
| 52 | + * @return 枚举 | ||
| 53 | + */ | ||
| 54 | + public static IotModbusByteOrderEnum getByOrder(String order) { | ||
| 55 | + return Arrays.stream(values()) | ||
| 56 | + .filter(e -> e.getOrder().equals(order)) | ||
| 57 | + .findFirst() | ||
| 58 | + .orElse(null); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | +} |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.enums.device; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; | ||
| 4 | +import lombok.Getter; | ||
| 5 | +import lombok.RequiredArgsConstructor; | ||
| 6 | + | ||
| 7 | +import java.util.Arrays; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * IoT Modbus 功能码枚举 | ||
| 11 | + * | ||
| 12 | + * @author 芋道源码 | ||
| 13 | + */ | ||
| 14 | +@Getter | ||
| 15 | +@RequiredArgsConstructor | ||
| 16 | +public enum IotModbusFunctionCodeEnum implements ArrayValuable<Integer> { | ||
| 17 | + | ||
| 18 | + READ_COILS(1, "读线圈", true, 5, 15), | ||
| 19 | + READ_DISCRETE_INPUTS(2, "读离散输入", false, null, null), | ||
| 20 | + READ_HOLDING_REGISTERS(3, "读保持寄存器", true, 6, 16), | ||
| 21 | + READ_INPUT_REGISTERS(4, "读输入寄存器", false, null, null); | ||
| 22 | + | ||
| 23 | + public static final Integer[] ARRAYS = Arrays.stream(values()) | ||
| 24 | + .map(IotModbusFunctionCodeEnum::getCode) | ||
| 25 | + .toArray(Integer[]::new); | ||
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * 功能码 | ||
| 29 | + */ | ||
| 30 | + private final Integer code; | ||
| 31 | + /** | ||
| 32 | + * 名称 | ||
| 33 | + */ | ||
| 34 | + private final String name; | ||
| 35 | + /** | ||
| 36 | + * 是否支持写操作 | ||
| 37 | + */ | ||
| 38 | + private final Boolean writable; | ||
| 39 | + /** | ||
| 40 | + * 单个写功能码 | ||
| 41 | + */ | ||
| 42 | + private final Integer writeSingleCode; | ||
| 43 | + /** | ||
| 44 | + * 多个写功能码 | ||
| 45 | + */ | ||
| 46 | + private final Integer writeMultipleCode; | ||
| 47 | + | ||
| 48 | + @Override | ||
| 49 | + public Integer[] array() { | ||
| 50 | + return ARRAYS; | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + /** | ||
| 54 | + * 根据功能码获取枚举 | ||
| 55 | + * | ||
| 56 | + * @param code 功能码 | ||
| 57 | + * @return 枚举 | ||
| 58 | + */ | ||
| 59 | + public static IotModbusFunctionCodeEnum valueOf(Integer code) { | ||
| 60 | + return Arrays.stream(values()) | ||
| 61 | + .filter(e -> e.getCode().equals(code)) | ||
| 62 | + .findFirst() | ||
| 63 | + .orElse(null); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + // TODO @AI:如果用不到,可以暂时删除哈; | ||
| 67 | + /** | ||
| 68 | + * 获取写功能码 | ||
| 69 | + * | ||
| 70 | + * @param registerCount 寄存器数量 | ||
| 71 | + * @return 写功能码 | ||
| 72 | + */ | ||
| 73 | + public Integer getWriteCode(int registerCount) { | ||
| 74 | + if (!writable) { | ||
| 75 | + return null; | ||
| 76 | + } | ||
| 77 | + return registerCount == 1 ? writeSingleCode : writeMultipleCode; | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | +} |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.enums.device; | ||
| 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", "字符串", -1); // -1 表示可变长度 | ||
| 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 | + // TODO @AI:去掉 default 会好点。null 表示可变; | ||
| 40 | + /** | ||
| 41 | + * 默认寄存器数量(-1 表示可变) | ||
| 42 | + */ | ||
| 43 | + private final Integer defaultRegisterCount; | ||
| 44 | + | ||
| 45 | + @Override | ||
| 46 | + public String[] array() { | ||
| 47 | + return ARRAYS; | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + // TODO @AI:如果不用,可以删除掉 | ||
| 51 | + /** | ||
| 52 | + * 根据类型获取枚举 | ||
| 53 | + * | ||
| 54 | + * @param type 类型 | ||
| 55 | + * @return 枚举 | ||
| 56 | + */ | ||
| 57 | + public static IotModbusRawDataTypeEnum getByType(String type) { | ||
| 58 | + return Arrays.stream(values()) | ||
| 59 | + .filter(e -> e.getType().equals(type)) | ||
| 60 | + .findFirst() | ||
| 61 | + .orElse(null); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | +} |
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.framework.common.pojo.PageResult; | ||
| 4 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; | ||
| 5 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; | ||
| 6 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; | ||
| 7 | +import jakarta.validation.Valid; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * IoT 设备 Modbus 连接配置 Service 接口 | ||
| 13 | + * | ||
| 14 | + * @author 芋道源码 | ||
| 15 | + */ | ||
| 16 | +public interface IotDeviceModbusConfigService { | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 创建设备 Modbus 连接配置 | ||
| 20 | + * | ||
| 21 | + * @param createReqVO 创建信息 | ||
| 22 | + * @return 编号 | ||
| 23 | + */ | ||
| 24 | + Long createModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO createReqVO); | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 更新设备 Modbus 连接配置 | ||
| 28 | + * | ||
| 29 | + * @param updateReqVO 更新信息 | ||
| 30 | + */ | ||
| 31 | + void updateModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO updateReqVO); | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 删除设备 Modbus 连接配置 | ||
| 35 | + * | ||
| 36 | + * @param id 编号 | ||
| 37 | + */ | ||
| 38 | + void deleteModbusConfig(Long id); | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 获得设备 Modbus 连接配置 | ||
| 42 | + * | ||
| 43 | + * @param id 编号 | ||
| 44 | + * @return 设备 Modbus 连接配置 | ||
| 45 | + */ | ||
| 46 | + IotDeviceModbusConfigDO getModbusConfig(Long id); | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * 根据设备编号获得 Modbus 连接配置 | ||
| 50 | + * | ||
| 51 | + * @param deviceId 设备编号 | ||
| 52 | + * @return 设备 Modbus 连接配置 | ||
| 53 | + */ | ||
| 54 | + IotDeviceModbusConfigDO getModbusConfigByDeviceId(Long deviceId); | ||
| 55 | + | ||
| 56 | + /** | ||
| 57 | + * 获得设备 Modbus 连接配置分页 | ||
| 58 | + * | ||
| 59 | + * @param pageReqVO 分页查询 | ||
| 60 | + * @return 设备 Modbus 连接配置分页 | ||
| 61 | + */ | ||
| 62 | + PageResult<IotDeviceModbusConfigDO> getModbusConfigPage(IotDeviceModbusConfigPageReqVO pageReqVO); | ||
| 63 | + | ||
| 64 | + /** | ||
| 65 | + * 获得所有启用的 Modbus 连接配置列表 | ||
| 66 | + * | ||
| 67 | + * @return 启用的 Modbus 连接配置列表 | ||
| 68 | + */ | ||
| 69 | + List<IotDeviceModbusConfigDO> getEnabledModbusConfigList(); | ||
| 70 | + | ||
| 71 | +} |
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.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||
| 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.IotDeviceModbusConfigPageReqVO; | ||
| 7 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; | ||
| 8 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; | ||
| 9 | +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusConfigMapper; | ||
| 10 | +import jakarta.annotation.Resource; | ||
| 11 | +import org.springframework.stereotype.Service; | ||
| 12 | +import org.springframework.validation.annotation.Validated; | ||
| 13 | + | ||
| 14 | +import java.util.List; | ||
| 15 | + | ||
| 16 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | ||
| 17 | +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODBUS_CONFIG_EXISTS; | ||
| 18 | +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODBUS_CONFIG_NOT_EXISTS; | ||
| 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 | + | ||
| 35 | + // TODO @AI:是不是搞成 save 接口?因为前端也不知道是 create 还是 update; | ||
| 36 | + @Override | ||
| 37 | + public Long createModbusConfig(IotDeviceModbusConfigSaveReqVO createReqVO) { | ||
| 38 | + // 1.1 校验设备存在 | ||
| 39 | + deviceService.validateDeviceExists(createReqVO.getDeviceId()); | ||
| 40 | + // 1.2 校验设备是否已有 Modbus 配置 | ||
| 41 | + validateModbusConfigUnique(createReqVO.getDeviceId(), null); | ||
| 42 | + | ||
| 43 | + // 2. 插入 | ||
| 44 | + IotDeviceModbusConfigDO modbusConfig = BeanUtils.toBean(createReqVO, IotDeviceModbusConfigDO.class); | ||
| 45 | + setDefaultValues(modbusConfig); | ||
| 46 | + modbusConfigMapper.insert(modbusConfig); | ||
| 47 | + return modbusConfig.getId(); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + @Override | ||
| 51 | + public void updateModbusConfig(IotDeviceModbusConfigSaveReqVO updateReqVO) { | ||
| 52 | + // 1.1 校验存在 | ||
| 53 | + validateModbusConfigExists(updateReqVO.getId()); | ||
| 54 | + // 1.2 校验设备存在 | ||
| 55 | + deviceService.validateDeviceExists(updateReqVO.getDeviceId()); | ||
| 56 | + // 1.3 校验唯一性 | ||
| 57 | + validateModbusConfigUnique(updateReqVO.getDeviceId(), updateReqVO.getId()); | ||
| 58 | + | ||
| 59 | + // 2. 更新 | ||
| 60 | + IotDeviceModbusConfigDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusConfigDO.class); | ||
| 61 | + modbusConfigMapper.updateById(updateObj); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + @Override | ||
| 65 | + public void deleteModbusConfig(Long id) { | ||
| 66 | + // 校验存在 | ||
| 67 | + validateModbusConfigExists(id); | ||
| 68 | + // 删除 | ||
| 69 | + modbusConfigMapper.deleteById(id); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + private void validateModbusConfigExists(Long id) { | ||
| 73 | + if (modbusConfigMapper.selectById(id) == null) { | ||
| 74 | + throw exception(DEVICE_MODBUS_CONFIG_NOT_EXISTS); | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + private void validateModbusConfigUnique(Long deviceId, Long excludeId) { | ||
| 79 | + IotDeviceModbusConfigDO config = modbusConfigMapper.selectByDeviceId(deviceId); | ||
| 80 | + // TODO @AI:ObjUtil notequals | ||
| 81 | + if (config != null && !config.getId().equals(excludeId)) { | ||
| 82 | + throw exception(DEVICE_MODBUS_CONFIG_EXISTS); | ||
| 83 | + } | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + // TODO @AI:不要这个;前端都必须传递; | ||
| 87 | + private void setDefaultValues(IotDeviceModbusConfigDO config) { | ||
| 88 | + if (config.getPort() == null) { | ||
| 89 | + config.setPort(502); | ||
| 90 | + } | ||
| 91 | + if (config.getSlaveId() == null) { | ||
| 92 | + config.setSlaveId(1); | ||
| 93 | + } | ||
| 94 | + if (config.getTimeout() == null) { | ||
| 95 | + config.setTimeout(3000); | ||
| 96 | + } | ||
| 97 | + if (config.getRetryInterval() == null) { | ||
| 98 | + config.setRetryInterval(1000); | ||
| 99 | + } | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + @Override | ||
| 103 | + public IotDeviceModbusConfigDO getModbusConfig(Long id) { | ||
| 104 | + return modbusConfigMapper.selectById(id); | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + @Override | ||
| 108 | + public IotDeviceModbusConfigDO getModbusConfigByDeviceId(Long deviceId) { | ||
| 109 | + return modbusConfigMapper.selectByDeviceId(deviceId); | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + @Override | ||
| 113 | + public PageResult<IotDeviceModbusConfigDO> getModbusConfigPage(IotDeviceModbusConfigPageReqVO pageReqVO) { | ||
| 114 | + return modbusConfigMapper.selectPage(pageReqVO); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + @Override | ||
| 118 | + public List<IotDeviceModbusConfigDO> getEnabledModbusConfigList() { | ||
| 119 | + return modbusConfigMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | +} |
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.List; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * IoT 设备 Modbus 点位配置 Service 接口 | ||
| 13 | + * | ||
| 14 | + * @author 芋道源码 | ||
| 15 | + */ | ||
| 16 | +public interface IotDeviceModbusPointService { | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 创建设备 Modbus 点位配置 | ||
| 20 | + * | ||
| 21 | + * @param createReqVO 创建信息 | ||
| 22 | + * @return 编号 | ||
| 23 | + */ | ||
| 24 | + Long createModbusPoint(@Valid IotDeviceModbusPointSaveReqVO createReqVO); | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 更新设备 Modbus 点位配置 | ||
| 28 | + * | ||
| 29 | + * @param updateReqVO 更新信息 | ||
| 30 | + */ | ||
| 31 | + void updateModbusPoint(@Valid IotDeviceModbusPointSaveReqVO updateReqVO); | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 删除设备 Modbus 点位配置 | ||
| 35 | + * | ||
| 36 | + * @param id 编号 | ||
| 37 | + */ | ||
| 38 | + void deleteModbusPoint(Long id); | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 获得设备 Modbus 点位配置 | ||
| 42 | + * | ||
| 43 | + * @param id 编号 | ||
| 44 | + * @return 设备 Modbus 点位配置 | ||
| 45 | + */ | ||
| 46 | + IotDeviceModbusPointDO getModbusPoint(Long id); | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * 获得设备 Modbus 点位配置分页 | ||
| 50 | + * | ||
| 51 | + * @param pageReqVO 分页查询 | ||
| 52 | + * @return 设备 Modbus 点位配置分页 | ||
| 53 | + */ | ||
| 54 | + PageResult<IotDeviceModbusPointDO> getModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO); | ||
| 55 | + | ||
| 56 | + /** | ||
| 57 | + * 根据设备编号获得点位配置列表 | ||
| 58 | + * | ||
| 59 | + * @param deviceId 设备编号 | ||
| 60 | + * @return 点位配置列表 | ||
| 61 | + */ | ||
| 62 | + List<IotDeviceModbusPointDO> getModbusPointListByDeviceId(Long deviceId); | ||
| 63 | + | ||
| 64 | + /** | ||
| 65 | + * 根据设备编号获得启用的点位配置列表 | ||
| 66 | + * | ||
| 67 | + * @param deviceId 设备编号 | ||
| 68 | + * @return 启用的点位配置列表 | ||
| 69 | + */ | ||
| 70 | + List<IotDeviceModbusPointDO> getEnabledModbusPointListByDeviceId(Long deviceId); | ||
| 71 | + | ||
| 72 | +} |
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.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||
| 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.IotDeviceModbusPointSaveReqVO; | ||
| 8 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; | ||
| 9 | +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; | ||
| 10 | +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusPointMapper; | ||
| 11 | +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; | ||
| 12 | +import jakarta.annotation.Resource; | ||
| 13 | +import org.springframework.stereotype.Service; | ||
| 14 | +import org.springframework.validation.annotation.Validated; | ||
| 15 | + | ||
| 16 | +import java.math.BigDecimal; | ||
| 17 | +import java.util.List; | ||
| 18 | + | ||
| 19 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | ||
| 20 | +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; | ||
| 21 | + | ||
| 22 | +/** | ||
| 23 | + * IoT 设备 Modbus 点位配置 Service 实现类 | ||
| 24 | + * | ||
| 25 | + * @author 芋道源码 | ||
| 26 | + */ | ||
| 27 | +@Service | ||
| 28 | +@Validated | ||
| 29 | +public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointService { | ||
| 30 | + | ||
| 31 | + @Resource | ||
| 32 | + private IotDeviceModbusPointMapper modbusPointMapper; | ||
| 33 | + | ||
| 34 | + @Resource | ||
| 35 | + private IotDeviceService deviceService; | ||
| 36 | + | ||
| 37 | + @Resource | ||
| 38 | + private IotThingModelService thingModelService; | ||
| 39 | + | ||
| 40 | + @Override | ||
| 41 | + public Long createModbusPoint(IotDeviceModbusPointSaveReqVO createReqVO) { | ||
| 42 | + // 1.1 校验设备存在 | ||
| 43 | + deviceService.validateDeviceExists(createReqVO.getDeviceId()); | ||
| 44 | + // 1.2 校验物模型属性存在 | ||
| 45 | + IotThingModelDO thingModel = validateThingModelExists(createReqVO.getThingModelId()); | ||
| 46 | + // 1.3 校验同一设备下点位唯一性(基于 identifier) | ||
| 47 | + validateModbusPointUnique(createReqVO.getDeviceId(), thingModel.getIdentifier(), null); | ||
| 48 | + | ||
| 49 | + // 2. 插入 | ||
| 50 | + IotDeviceModbusPointDO modbusPoint = BeanUtils.toBean(createReqVO, IotDeviceModbusPointDO.class, | ||
| 51 | + o -> o.setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName())); | ||
| 52 | + modbusPointMapper.insert(modbusPoint); | ||
| 53 | + return modbusPoint.getId(); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public void updateModbusPoint(IotDeviceModbusPointSaveReqVO updateReqVO) { | ||
| 58 | + // 1.1 校验存在 | ||
| 59 | + validateModbusPointExists(updateReqVO.getId()); | ||
| 60 | + // 1.2 校验设备存在 | ||
| 61 | + deviceService.validateDeviceExists(updateReqVO.getDeviceId()); | ||
| 62 | + // 1.3 校验物模型属性存在 | ||
| 63 | + IotThingModelDO thingModel = validateThingModelExists(updateReqVO.getThingModelId()); | ||
| 64 | + // 1.4 校验同一设备下点位唯一性 | ||
| 65 | + validateModbusPointUnique(updateReqVO.getDeviceId(), thingModel.getIdentifier(), updateReqVO.getId()); | ||
| 66 | + | ||
| 67 | + // 2. 更新 | ||
| 68 | + IotDeviceModbusPointDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusPointDO.class); | ||
| 69 | + // TODO @AI:这块 | ||
| 70 | + modbusPointMapper.updateById(updateObj); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + private IotThingModelDO validateThingModelExists(Long id) { | ||
| 74 | + IotThingModelDO thingModel = thingModelService.getThingModel(id); | ||
| 75 | + if (thingModel == null) { | ||
| 76 | + throw exception(THING_MODEL_NOT_EXISTS); | ||
| 77 | + } | ||
| 78 | + return thingModel; | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + @Override | ||
| 82 | + public void deleteModbusPoint(Long id) { | ||
| 83 | + // 校验存在 | ||
| 84 | + validateModbusPointExists(id); | ||
| 85 | + // 删除 | ||
| 86 | + modbusPointMapper.deleteById(id); | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + private IotDeviceModbusPointDO validateModbusPointExists(Long id) { | ||
| 90 | + IotDeviceModbusPointDO point = modbusPointMapper.selectById(id); | ||
| 91 | + if (point == null) { | ||
| 92 | + throw exception(DEVICE_MODBUS_POINT_NOT_EXISTS); | ||
| 93 | + } | ||
| 94 | + return point; | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + private void validateModbusPointUnique(Long deviceId, String identifier, Long excludeId) { | ||
| 98 | + IotDeviceModbusPointDO point = modbusPointMapper.selectByDeviceIdAndIdentifier(deviceId, identifier); | ||
| 99 | + // TODO @AI:ObjUtil notequals; | ||
| 100 | + if (point != null && !point.getId().equals(excludeId)) { | ||
| 101 | + throw exception(DEVICE_MODBUS_POINT_EXISTS); | ||
| 102 | + } | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + // TODO @AI:这块 | ||
| 106 | + private void setDefaultValues(IotDeviceModbusPointDO point) { | ||
| 107 | + if (point.getRegisterCount() == null) { | ||
| 108 | + point.setRegisterCount(1); | ||
| 109 | + } | ||
| 110 | + if (point.getScale() == null) { | ||
| 111 | + point.setScale(BigDecimal.ONE); | ||
| 112 | + } | ||
| 113 | + if (point.getPollInterval() == null) { | ||
| 114 | + point.setPollInterval(5000); | ||
| 115 | + } | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + @Override | ||
| 119 | + public IotDeviceModbusPointDO getModbusPoint(Long id) { | ||
| 120 | + return modbusPointMapper.selectById(id); | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + @Override | ||
| 124 | + public PageResult<IotDeviceModbusPointDO> getModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO) { | ||
| 125 | + return modbusPointMapper.selectPage(pageReqVO); | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + @Override | ||
| 129 | + public List<IotDeviceModbusPointDO> getModbusPointListByDeviceId(Long deviceId) { | ||
| 130 | + return modbusPointMapper.selectListByDeviceId(deviceId); | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + @Override | ||
| 134 | + public List<IotDeviceModbusPointDO> getEnabledModbusPointListByDeviceId(Long deviceId) { | ||
| 135 | + return modbusPointMapper.selectListByDeviceIdAndStatus(deviceId, CommonStatusEnum.ENABLE.getStatus()); | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | +} |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java
| @@ -4,6 +4,9 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; | @@ -4,6 +4,9 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 7 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 7 | 10 | ||
| 8 | /** | 11 | /** |
| 9 | * IoT 设备通用 API | 12 | * IoT 设备通用 API |
| @@ -28,4 +31,11 @@ public interface IotDeviceCommonApi { | @@ -28,4 +31,11 @@ public interface IotDeviceCommonApi { | ||
| 28 | */ | 31 | */ |
| 29 | CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO infoReqDTO); | 32 | CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO infoReqDTO); |
| 30 | 33 | ||
| 34 | + /** | ||
| 35 | + * 获取所有启用的 Modbus 设备配置列表 | ||
| 36 | + * | ||
| 37 | + * @return Modbus 设备配置列表 | ||
| 38 | + */ | ||
| 39 | + CommonResult<List<IotModbusDeviceConfigRespDTO>> getEnabledModbusDeviceConfigs(); | ||
| 40 | + | ||
| 31 | } | 41 | } |
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 | + * 租户编号 | ||
| 29 | + */ | ||
| 30 | + private Long tenantId; | ||
| 31 | + | ||
| 32 | + // ========== Modbus 连接配置 ========== | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * Modbus 服务器 IP 地址 | ||
| 36 | + */ | ||
| 37 | + private String ip; | ||
| 38 | + /** | ||
| 39 | + * Modbus 端口 | ||
| 40 | + */ | ||
| 41 | + private Integer port; | ||
| 42 | + /** | ||
| 43 | + * 从站地址 | ||
| 44 | + */ | ||
| 45 | + private Integer slaveId; | ||
| 46 | + /** | ||
| 47 | + * 连接超时时间(毫秒) | ||
| 48 | + */ | ||
| 49 | + private Integer timeout; | ||
| 50 | + /** | ||
| 51 | + * 重试间隔(毫秒) | ||
| 52 | + */ | ||
| 53 | + private Integer retryInterval; | ||
| 54 | + | ||
| 55 | + // ========== 点位配置 ========== | ||
| 56 | + | ||
| 57 | + /** | ||
| 58 | + * 点位列表 | ||
| 59 | + */ | ||
| 60 | + private List<IotModbusPointRespDTO> points; | ||
| 61 | + | ||
| 62 | +} |
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 lombok.Data; | ||
| 4 | + | ||
| 5 | +import java.math.BigDecimal; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * IoT Modbus 点位配置 Response DTO | ||
| 9 | + * | ||
| 10 | + * @author 芋道源码 | ||
| 11 | + */ | ||
| 12 | +@Data | ||
| 13 | +public class IotModbusPointRespDTO { | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * 点位编号 | ||
| 17 | + */ | ||
| 18 | + private Long id; | ||
| 19 | + /** | ||
| 20 | + * 属性标识符(物模型的 identifier) | ||
| 21 | + */ | ||
| 22 | + private String identifier; | ||
| 23 | + /** | ||
| 24 | + * 属性名称(物模型的 name) | ||
| 25 | + */ | ||
| 26 | + private String name; | ||
| 27 | + | ||
| 28 | + // ========== Modbus 协议配置 ========== | ||
| 29 | + | ||
| 30 | + // TODO @AI:所有的枚举,通过 @,不要写上去; | ||
| 31 | + /** | ||
| 32 | + * Modbus 功能码 | ||
| 33 | + * | ||
| 34 | + * 1-ReadCoils 2-ReadDiscreteInputs 3-ReadHoldingRegisters 4-ReadInputRegisters | ||
| 35 | + */ | ||
| 36 | + private Integer functionCode; | ||
| 37 | + /** | ||
| 38 | + * 寄存器起始地址 | ||
| 39 | + */ | ||
| 40 | + private Integer registerAddress; | ||
| 41 | + /** | ||
| 42 | + * 寄存器数量 | ||
| 43 | + */ | ||
| 44 | + private Integer registerCount; | ||
| 45 | + /** | ||
| 46 | + * 字节序 | ||
| 47 | + * | ||
| 48 | + * AB/BA/ABCD/CDAB/DCBA/BADC | ||
| 49 | + */ | ||
| 50 | + private String byteOrder; | ||
| 51 | + /** | ||
| 52 | + * 原始数据类型 | ||
| 53 | + * | ||
| 54 | + * INT16/UINT16/INT32/UINT32/FLOAT/DOUBLE/BOOLEAN/STRING | ||
| 55 | + */ | ||
| 56 | + private String rawDataType; | ||
| 57 | + /** | ||
| 58 | + * 缩放因子 | ||
| 59 | + */ | ||
| 60 | + private BigDecimal scale; | ||
| 61 | + /** | ||
| 62 | + * 轮询间隔(毫秒) | ||
| 63 | + */ | ||
| 64 | + private Integer pollInterval; | ||
| 65 | + | ||
| 66 | + // ========== 物模型相关字段 ========== | ||
| 67 | + | ||
| 68 | + // TODO @AI:分析一下,是否有必要返回 | ||
| 69 | + /** | ||
| 70 | + * 数据类型(来自物模型) | ||
| 71 | + */ | ||
| 72 | + private String dataType; | ||
| 73 | + | ||
| 74 | +} |
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 | <!-- 测试相关 --> | 58 | <!-- 测试相关 --> |
| 52 | <dependency> | 59 | <dependency> |
| 53 | <groupId>cn.iocoder.boot</groupId> | 60 | <groupId>cn.iocoder.boot</groupId> |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscr | @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscr | ||
| 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; |
| 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; |
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.*; | ||
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; |
| 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; |
| 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| @@ -194,4 +195,38 @@ public class IotGatewayConfiguration { | @@ -194,4 +195,38 @@ public class IotGatewayConfiguration { | ||
| 194 | 195 | ||
| 195 | } | 196 | } |
| 196 | 197 | ||
| 198 | + /** | ||
| 199 | + * IoT 网关 Modbus TCP 协议配置类 | ||
| 200 | + */ | ||
| 201 | + @Configuration | ||
| 202 | + @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.modbus-tcp", name = "enabled", havingValue = "true") | ||
| 203 | + @Slf4j | ||
| 204 | + public static class ModbusTcpProtocolConfiguration { | ||
| 205 | + | ||
| 206 | + @Bean(name = "modbusTcpVertx", destroyMethod = "close") | ||
| 207 | + public Vertx modbusTcpVertx() { | ||
| 208 | + return Vertx.vertx(); | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + @Bean | ||
| 212 | + public IotModbusTcpUpstreamProtocol iotModbusTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, | ||
| 213 | + IotDeviceMessageService messageService, | ||
| 214 | + IotModbusTcpConnectionManager connectionManager, | ||
| 215 | + IotModbusTcpPollScheduler pollScheduler, | ||
| 216 | + IotModbusTcpConfigCacheService configCacheService, | ||
| 217 | + IotModbusTcpUpstreamHandler upstreamHandler, | ||
| 218 | + @Qualifier("modbusTcpVertx") Vertx modbusTcpVertx) { | ||
| 219 | + return new IotModbusTcpUpstreamProtocol(gatewayProperties.getProtocol().getModbusTcp(), | ||
| 220 | + messageService, connectionManager, pollScheduler, configCacheService, upstreamHandler, modbusTcpVertx); | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + @Bean | ||
| 224 | + public IotModbusTcpDownstreamSubscriber iotModbusTcpDownstreamSubscriber(IotModbusTcpUpstreamProtocol upstreamProtocol, | ||
| 225 | + IotModbusTcpDownstreamHandler downstreamHandler, | ||
| 226 | + IotMessageBus messageBus) { | ||
| 227 | + return new IotModbusTcpDownstreamSubscriber(upstreamProtocol, downstreamHandler, messageBus); | ||
| 228 | + } | ||
| 229 | + | ||
| 230 | + } | ||
| 231 | + | ||
| 197 | } | 232 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| @@ -93,6 +93,11 @@ public class IotGatewayProperties { | @@ -93,6 +93,11 @@ public class IotGatewayProperties { | ||
| 93 | */ | 93 | */ |
| 94 | private MqttWsProperties mqttWs; | 94 | private MqttWsProperties mqttWs; |
| 95 | 95 | ||
| 96 | + /** | ||
| 97 | + * Modbus TCP 组件配置 | ||
| 98 | + */ | ||
| 99 | + private ModbusTcpProperties modbusTcp; | ||
| 100 | + | ||
| 96 | } | 101 | } |
| 97 | 102 | ||
| 98 | @Data | 103 | @Data |
| @@ -503,4 +508,21 @@ public class IotGatewayProperties { | @@ -503,4 +508,21 @@ public class IotGatewayProperties { | ||
| 503 | 508 | ||
| 504 | } | 509 | } |
| 505 | 510 | ||
| 511 | + @Data | ||
| 512 | + public static class ModbusTcpProperties { | ||
| 513 | + | ||
| 514 | + /** | ||
| 515 | + * 是否开启 | ||
| 516 | + */ | ||
| 517 | + @NotNull(message = "是否开启不能为空") | ||
| 518 | + private Boolean enabled; | ||
| 519 | + | ||
| 520 | + /** | ||
| 521 | + * 配置刷新间隔(秒) | ||
| 522 | + */ | ||
| 523 | + // TODO @AI:需要校验下非空; | ||
| 524 | + private Integer configRefreshInterval = 30; | ||
| 525 | + | ||
| 526 | + } | ||
| 527 | + | ||
| 506 | } | 528 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | +import org.springframework.stereotype.Component; | ||
| 6 | + | ||
| 7 | +import java.math.BigDecimal; | ||
| 8 | +import java.math.RoundingMode; | ||
| 9 | +import java.nio.ByteBuffer; | ||
| 10 | +import java.nio.ByteOrder; | ||
| 11 | + | ||
| 12 | +// TODO @AI:注释上:希望 1 和 2,然后 3 只是辅助说明 | ||
| 13 | +/** | ||
| 14 | + * IoT Modbus 数据转换器 | ||
| 15 | + * | ||
| 16 | + * 负责: | ||
| 17 | + * 1. 将 Modbus 原始寄存器值转换为物模型属性值 | ||
| 18 | + * 2. 将物模型属性值转换为 Modbus 原始寄存器值 | ||
| 19 | + * 3. 处理字节序、数据类型、缩放因子 | ||
| 20 | + * | ||
| 21 | + * @author 芋道源码 | ||
| 22 | + */ | ||
| 23 | +// TODO @AI:希望它的初始化,在 configuration 里; | ||
| 24 | +@Component | ||
| 25 | +@Slf4j | ||
| 26 | +public class IotModbusDataConverter { | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 将原始值转换为物模型属性值 | ||
| 30 | + * | ||
| 31 | + * @param rawValues 原始值数组(寄存器值或线圈值) | ||
| 32 | + * @param point 点位配置 | ||
| 33 | + * @return 转换后的属性值 | ||
| 34 | + */ | ||
| 35 | + public Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) { | ||
| 36 | + // TODO @AI:CollUtil.isEmpty; | ||
| 37 | + if (rawValues == null || rawValues.length == 0) { | ||
| 38 | + return null; | ||
| 39 | + } | ||
| 40 | + String rawDataType = point.getRawDataType(); | ||
| 41 | + String byteOrder = point.getByteOrder(); | ||
| 42 | + // TODO @AI:defaultIfNull; | ||
| 43 | + BigDecimal scale = point.getScale() != null ? point.getScale() : BigDecimal.ONE; | ||
| 44 | + | ||
| 45 | + // 1. 根据原始数据类型解析原始数值 | ||
| 46 | + Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder); | ||
| 47 | + if (rawNumber == null) { | ||
| 48 | + return null; | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + // 2. 应用缩放因子:实际值 = 原始值 × scale | ||
| 52 | + BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale); | ||
| 53 | + | ||
| 54 | + // 3. 根据数据类型返回合适的 Java 类型 | ||
| 55 | + return formatValue(actualValue, rawDataType); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * 将物模型属性值转换为原始寄存器值 | ||
| 60 | + * | ||
| 61 | + * @param propertyValue 属性值 | ||
| 62 | + * @param point 点位配置 | ||
| 63 | + * @return 原始值数组 | ||
| 64 | + */ | ||
| 65 | + public int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) { | ||
| 66 | + if (propertyValue == null) { | ||
| 67 | + return new int[0]; | ||
| 68 | + } | ||
| 69 | + String rawDataType = point.getRawDataType(); | ||
| 70 | + String byteOrder = point.getByteOrder(); | ||
| 71 | + BigDecimal scale = point.getScale() != null ? point.getScale() : BigDecimal.ONE; | ||
| 72 | + int registerCount = point.getRegisterCount() != null ? point.getRegisterCount() : 1; | ||
| 73 | + | ||
| 74 | + // 1. 转换为 BigDecimal | ||
| 75 | + BigDecimal actualValue = new BigDecimal(propertyValue.toString()); | ||
| 76 | + | ||
| 77 | + // 2. 应用缩放因子:原始值 = 实际值 ÷ scale | ||
| 78 | + BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP); | ||
| 79 | + | ||
| 80 | + // 3. 根据原始数据类型编码为寄存器值 | ||
| 81 | + return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * 解析原始值 | ||
| 86 | + */ | ||
| 87 | + private Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) { | ||
| 88 | + // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的; | ||
| 89 | + switch (rawDataType.toUpperCase()) { | ||
| 90 | + case "BOOLEAN": | ||
| 91 | + return rawValues[0] != 0 ? 1 : 0; | ||
| 92 | + case "INT16": | ||
| 93 | + return (short) rawValues[0]; | ||
| 94 | + case "UINT16": | ||
| 95 | + return rawValues[0] & 0xFFFF; | ||
| 96 | + case "INT32": | ||
| 97 | + return parseInt32(rawValues, byteOrder); | ||
| 98 | + case "UINT32": | ||
| 99 | + return parseUint32(rawValues, byteOrder); | ||
| 100 | + case "FLOAT": | ||
| 101 | + return parseFloat(rawValues, byteOrder); | ||
| 102 | + case "DOUBLE": | ||
| 103 | + return parseDouble(rawValues, byteOrder); | ||
| 104 | + // TODO @AI:未知抛出异常; | ||
| 105 | + default: | ||
| 106 | + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); | ||
| 107 | + return rawValues[0]; | ||
| 108 | + } | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + // TODO @AI:这些转换,有没一些工具类,可以优化;类似 hutool 的? | ||
| 112 | + private int parseInt32(int[] rawValues, String byteOrder) { | ||
| 113 | + if (rawValues.length < 2) { | ||
| 114 | + return rawValues[0]; | ||
| 115 | + } | ||
| 116 | + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); | ||
| 117 | + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt(); | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + private long parseUint32(int[] rawValues, String byteOrder) { | ||
| 121 | + if (rawValues.length < 2) { | ||
| 122 | + return rawValues[0] & 0xFFFFFFFFL; | ||
| 123 | + } | ||
| 124 | + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); | ||
| 125 | + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL; | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + private float parseFloat(int[] rawValues, String byteOrder) { | ||
| 129 | + if (rawValues.length < 2) { | ||
| 130 | + return (float) rawValues[0]; | ||
| 131 | + } | ||
| 132 | + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); | ||
| 133 | + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat(); | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + private double parseDouble(int[] rawValues, String byteOrder) { | ||
| 137 | + if (rawValues.length < 4) { | ||
| 138 | + return rawValues[0]; | ||
| 139 | + } | ||
| 140 | + byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder); | ||
| 141 | + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble(); | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + /** | ||
| 145 | + * 将寄存器值转换为字节数组 | ||
| 146 | + */ | ||
| 147 | + private byte[] registersToBytes(int[] registers, int count) { | ||
| 148 | + byte[] bytes = new byte[count * 2]; | ||
| 149 | + for (int i = 0; i < Math.min(registers.length, count); i++) { | ||
| 150 | + bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF); | ||
| 151 | + bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF); | ||
| 152 | + } | ||
| 153 | + return bytes; | ||
| 154 | + } | ||
| 155 | + | ||
| 156 | + /** | ||
| 157 | + * 根据字节序重排字节 | ||
| 158 | + */ | ||
| 159 | + private byte[] reorderBytes(byte[] bytes, String byteOrder) { | ||
| 160 | + // 大端序,不需要调整 | ||
| 161 | + // TODO @AI:StrUtil.equals;null 要抛出异常; | ||
| 162 | + if (byteOrder == null || "ABCD".equals(byteOrder) || "AB".equals(byteOrder)) { | ||
| 163 | + return bytes; | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + // 其他字节序调整 | ||
| 167 | + byte[] result = new byte[bytes.length]; | ||
| 168 | + switch (byteOrder.toUpperCase()) { | ||
| 169 | + // TODO @AI:走枚举;sortOrder; | ||
| 170 | + case "BA": // 小端序(16 位) | ||
| 171 | + if (bytes.length >= 2) { | ||
| 172 | + result[0] = bytes[1]; | ||
| 173 | + result[1] = bytes[0]; | ||
| 174 | + } | ||
| 175 | + break; | ||
| 176 | + case "CDAB": // 大端字交换(32 位) | ||
| 177 | + if (bytes.length >= 4) { | ||
| 178 | + result[0] = bytes[2]; | ||
| 179 | + result[1] = bytes[3]; | ||
| 180 | + result[2] = bytes[0]; | ||
| 181 | + result[3] = bytes[1]; | ||
| 182 | + } | ||
| 183 | + break; | ||
| 184 | + case "DCBA": // 小端序(32 位) | ||
| 185 | + if (bytes.length >= 4) { | ||
| 186 | + result[0] = bytes[3]; | ||
| 187 | + result[1] = bytes[2]; | ||
| 188 | + result[2] = bytes[1]; | ||
| 189 | + result[3] = bytes[0]; | ||
| 190 | + } | ||
| 191 | + break; | ||
| 192 | + case "BADC": // 小端字交换(32 位) | ||
| 193 | + if (bytes.length >= 4) { | ||
| 194 | + result[0] = bytes[1]; | ||
| 195 | + result[1] = bytes[0]; | ||
| 196 | + result[2] = bytes[3]; | ||
| 197 | + result[3] = bytes[2]; | ||
| 198 | + } | ||
| 199 | + break; | ||
| 200 | + // TODO @AI:未知就抛出异常; | ||
| 201 | + default: | ||
| 202 | + return bytes; | ||
| 203 | + } | ||
| 204 | + return result; | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + /** | ||
| 208 | + * 编码为寄存器值 | ||
| 209 | + */ | ||
| 210 | + private int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) { | ||
| 211 | + // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的; | ||
| 212 | + switch (rawDataType.toUpperCase()) { | ||
| 213 | + case "BOOLEAN": | ||
| 214 | + return new int[]{rawValue.intValue() != 0 ? 1 : 0}; | ||
| 215 | + case "INT16": | ||
| 216 | + case "UINT16": | ||
| 217 | + return new int[]{rawValue.intValue() & 0xFFFF}; | ||
| 218 | + case "INT32": | ||
| 219 | + case "UINT32": | ||
| 220 | + return encodeInt32(rawValue.intValue(), byteOrder); | ||
| 221 | + case "FLOAT": | ||
| 222 | + return encodeFloat(rawValue.floatValue(), byteOrder); | ||
| 223 | + case "DOUBLE": | ||
| 224 | + return encodeDouble(rawValue.doubleValue(), byteOrder); | ||
| 225 | + default: | ||
| 226 | + return new int[]{rawValue.intValue()}; | ||
| 227 | + } | ||
| 228 | + } | ||
| 229 | + | ||
| 230 | + // TODO @AI:这些转换,有没一些工具类,可以优化;类似 hutool 的? | ||
| 231 | + private int[] encodeInt32(int value, String byteOrder) { | ||
| 232 | + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array(); | ||
| 233 | + bytes = reorderBytes(bytes, byteOrder); | ||
| 234 | + return bytesToRegisters(bytes); | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + private int[] encodeFloat(float value, String byteOrder) { | ||
| 238 | + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array(); | ||
| 239 | + bytes = reorderBytes(bytes, byteOrder); | ||
| 240 | + return bytesToRegisters(bytes); | ||
| 241 | + } | ||
| 242 | + | ||
| 243 | + private int[] encodeDouble(double value, String byteOrder) { | ||
| 244 | + byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array(); | ||
| 245 | + bytes = reorderBytes(bytes, byteOrder); | ||
| 246 | + return bytesToRegisters(bytes); | ||
| 247 | + } | ||
| 248 | + | ||
| 249 | + private int[] bytesToRegisters(byte[] bytes) { | ||
| 250 | + int[] registers = new int[bytes.length / 2]; | ||
| 251 | + for (int i = 0; i < registers.length; i++) { | ||
| 252 | + registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF); | ||
| 253 | + } | ||
| 254 | + return registers; | ||
| 255 | + } | ||
| 256 | + | ||
| 257 | + /** | ||
| 258 | + * 格式化返回值 | ||
| 259 | + */ | ||
| 260 | + private Object formatValue(BigDecimal value, String rawDataType) { | ||
| 261 | + // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的; | ||
| 262 | + switch (rawDataType.toUpperCase()) { | ||
| 263 | + case "BOOLEAN": | ||
| 264 | + return value.intValue() != 0; | ||
| 265 | + case "INT16": | ||
| 266 | + case "INT32": | ||
| 267 | + return value.intValue(); | ||
| 268 | + case "UINT16": | ||
| 269 | + case "UINT32": | ||
| 270 | + return value.longValue(); | ||
| 271 | + case "FLOAT": | ||
| 272 | + return value.floatValue(); | ||
| 273 | + case "DOUBLE": | ||
| 274 | + return value.doubleValue(); | ||
| 275 | + // TODO @AI:未知抛出异常; | ||
| 276 | + default: | ||
| 277 | + return value; | ||
| 278 | + } | ||
| 279 | + } | ||
| 280 | + | ||
| 281 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | ||
| 4 | +import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; | ||
| 5 | +import com.ghgande.j2mod.modbus.msg.*; | ||
| 6 | +import com.ghgande.j2mod.modbus.procimg.InputRegister; | ||
| 7 | +import com.ghgande.j2mod.modbus.procimg.Register; | ||
| 8 | +import com.ghgande.j2mod.modbus.procimg.SimpleRegister; | ||
| 9 | +import com.ghgande.j2mod.modbus.util.BitVector; | ||
| 10 | +import io.vertx.core.Future; | ||
| 11 | +import lombok.RequiredArgsConstructor; | ||
| 12 | +import lombok.extern.slf4j.Slf4j; | ||
| 13 | +import org.springframework.stereotype.Component; | ||
| 14 | + | ||
| 15 | +/** | ||
| 16 | + * IoT Modbus TCP 客户端 | ||
| 17 | + * | ||
| 18 | + * 负责: | ||
| 19 | + * 1. 封装 Modbus 读写操作 | ||
| 20 | + * 2. 根据功能码执行对应的 Modbus 请求 | ||
| 21 | + * | ||
| 22 | + * @author 芋道源码 | ||
| 23 | + */ | ||
| 24 | +// TODO @AI:希望它的初始化,在 configuration 里; | ||
| 25 | +@Component | ||
| 26 | +@RequiredArgsConstructor // TODO @AI:这个注解,是不是可以去掉? | ||
| 27 | +@Slf4j | ||
| 28 | +public class IotModbusTcpClient { | ||
| 29 | + | ||
| 30 | + /** | ||
| 31 | + * 读取 Modbus 数据 | ||
| 32 | + * | ||
| 33 | + * @param connection Modbus 连接 | ||
| 34 | + * @param slaveId 从站地址 | ||
| 35 | + * @param point 点位配置 | ||
| 36 | + * @return 原始值(int 数组) | ||
| 37 | + */ | ||
| 38 | + public Future<int[]> read(IotModbusTcpConnectionManager.ModbusConnection connection, | ||
| 39 | + Integer slaveId, | ||
| 40 | + IotModbusPointRespDTO point) { | ||
| 41 | + return connection.executeBlocking(tcpConnection -> { | ||
| 42 | + try { | ||
| 43 | + // 1. 创建请求 | ||
| 44 | + ModbusRequest request = createReadRequest(point.getFunctionCode(), | ||
| 45 | + point.getRegisterAddress(), point.getRegisterCount()); | ||
| 46 | + request.setUnitID(slaveId); | ||
| 47 | + | ||
| 48 | + // 2. 执行事务(请求) | ||
| 49 | + ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection); | ||
| 50 | + transaction.setRequest(request); | ||
| 51 | + transaction.execute(); | ||
| 52 | + | ||
| 53 | + // 3. 解析响应 | ||
| 54 | + ModbusResponse response = transaction.getResponse(); | ||
| 55 | + return extractValues(response, point.getFunctionCode()); | ||
| 56 | + } catch (Exception e) { | ||
| 57 | + // TODO @AI:抛出异常时,增加更多的上下文信息,比如设备、点位等 | ||
| 58 | + throw new RuntimeException("Modbus 读取失败", e); | ||
| 59 | + } | ||
| 60 | + }); | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 写入 Modbus 数据 | ||
| 65 | + * | ||
| 66 | + * @param connection Modbus 连接 | ||
| 67 | + * @param slaveId 从站地址 | ||
| 68 | + * @param point 点位配置 | ||
| 69 | + * @param values 要写入的值 | ||
| 70 | + * @return 是否成功 | ||
| 71 | + */ | ||
| 72 | + public Future<Boolean> write(IotModbusTcpConnectionManager.ModbusConnection connection, | ||
| 73 | + Integer slaveId, | ||
| 74 | + IotModbusPointRespDTO point, | ||
| 75 | + int[] values) { | ||
| 76 | + return connection.executeBlocking(tcpConnection -> { | ||
| 77 | + try { | ||
| 78 | + // 1. 创建请求 | ||
| 79 | + ModbusRequest request = createWriteRequest(point.getFunctionCode(), | ||
| 80 | + point.getRegisterAddress(), point.getRegisterCount(), values); | ||
| 81 | + if (request == null) { | ||
| 82 | + throw new RuntimeException("功能码 " + point.getFunctionCode() + " 不支持写操作"); | ||
| 83 | + } | ||
| 84 | + request.setUnitID(slaveId); | ||
| 85 | + | ||
| 86 | + // 2. 执行事务(请求) | ||
| 87 | + ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection); | ||
| 88 | + transaction.setRequest(request); | ||
| 89 | + transaction.execute(); | ||
| 90 | + return true; | ||
| 91 | + } catch (Exception e) { | ||
| 92 | + // TODO @AI:抛出异常时,增加更多的上下文信息,比如设备、点位等; | ||
| 93 | + throw new RuntimeException("Modbus 写入失败", e); | ||
| 94 | + } | ||
| 95 | + }); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + /** | ||
| 99 | + * 创建读取请求 | ||
| 100 | + */ | ||
| 101 | + private ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { | ||
| 102 | + // TODO @AI:1、2、3、4 能不能有枚举哈?这样 1、2、3、4 那的注释就不用写; | ||
| 103 | + switch (functionCode) { | ||
| 104 | + case 1: // ReadCoils | ||
| 105 | + return new ReadCoilsRequest(address, count); | ||
| 106 | + case 2: // ReadDiscreteInputs | ||
| 107 | + return new ReadInputDiscretesRequest(address, count); | ||
| 108 | + case 3: // ReadHoldingRegisters | ||
| 109 | + return new ReadMultipleRegistersRequest(address, count); | ||
| 110 | + case 4: // ReadInputRegisters | ||
| 111 | + return new ReadInputRegistersRequest(address, count); | ||
| 112 | + default: | ||
| 113 | + throw new IllegalArgumentException("不支持的功能码: " + functionCode); | ||
| 114 | + } | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + /** | ||
| 118 | + * 创建写入请求 | ||
| 119 | + */ | ||
| 120 | + private ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { | ||
| 121 | + // TODO @AI:5、6、15、16 能不能有枚举哈?这样 5、6、15、16 那的注释就不用写; | ||
| 122 | + switch (functionCode) { | ||
| 123 | + case 1: // WriteCoils (使用 5 或 15) | ||
| 124 | + if (count == 1) { | ||
| 125 | + return new WriteCoilRequest(address, values[0] != 0); | ||
| 126 | + } else { | ||
| 127 | + // 多线圈写入 | ||
| 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 3: // WriteHoldingRegisters (使用 6 或 16) | ||
| 135 | + if (count == 1) { | ||
| 136 | + return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0])); | ||
| 137 | + } else { | ||
| 138 | + Register[] registers = new com.ghgande.j2mod.modbus.procimg.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 2: // ReadDiscreteInputs - 只读 | ||
| 145 | + case 4: // ReadInputRegisters - 只读 | ||
| 146 | + return null; | ||
| 147 | + default: | ||
| 148 | + throw new IllegalArgumentException("不支持的功能码: " + functionCode); | ||
| 149 | + } | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + /** | ||
| 153 | + * 从响应中提取值 | ||
| 154 | + */ | ||
| 155 | + private int[] extractValues(ModbusResponse response, Integer functionCode) { | ||
| 156 | + // TODO @AI:1、2、3、4 能不能有枚举哈?这样 1、2、3、4 那的注释就不用写; | ||
| 157 | + switch (functionCode) { | ||
| 158 | + case 1: // ReadCoils | ||
| 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 2: // ReadDiscreteInputs | ||
| 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 3: // ReadHoldingRegisters | ||
| 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 4: // ReadInputRegisters | ||
| 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/modbustcp/IotModbusTcpConfigCacheService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | ||
| 6 | +import lombok.RequiredArgsConstructor; | ||
| 7 | +import lombok.extern.slf4j.Slf4j; | ||
| 8 | +import org.springframework.stereotype.Service; | ||
| 9 | + | ||
| 10 | +import java.util.*; | ||
| 11 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 12 | +import java.util.function.Consumer; | ||
| 13 | + | ||
| 14 | +// TODO @AI:是不是 1、2、3 注释可以合并下; | ||
| 15 | +/** | ||
| 16 | + * IoT Modbus TCP 配置缓存服务 | ||
| 17 | + * | ||
| 18 | + * 负责: | ||
| 19 | + * 1. 从 biz 拉取 Modbus 设备配置 | ||
| 20 | + * 2. 缓存配置数据 | ||
| 21 | + * 3. 检测配置变更 | ||
| 22 | + * | ||
| 23 | + * @author 芋道源码 | ||
| 24 | + */ | ||
| 25 | +// TODO @AI:希望它的初始化,在 configuration 里; | ||
| 26 | +@Service | ||
| 27 | +@RequiredArgsConstructor | ||
| 28 | +@Slf4j | ||
| 29 | +public class IotModbusTcpConfigCacheService { | ||
| 30 | + | ||
| 31 | + private final IotDeviceCommonApi deviceApi; | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 配置缓存:deviceId -> 配置 | ||
| 35 | + */ | ||
| 36 | + private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>(); | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 已知的设备 ID 集合 | ||
| 40 | + */ | ||
| 41 | + private final Set<Long> knownDeviceIds = ConcurrentHashMap.newKeySet(); | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 刷新配置 | ||
| 45 | + * | ||
| 46 | + * @return 最新的配置列表 | ||
| 47 | + */ | ||
| 48 | + public List<IotModbusDeviceConfigRespDTO> refreshConfig() { | ||
| 49 | + try { | ||
| 50 | + // 1. 从远程获取配置 | ||
| 51 | + CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getEnabledModbusDeviceConfigs(); | ||
| 52 | + if (result == null || !result.isSuccess() || result.getData() == null) { | ||
| 53 | + log.warn("[refreshConfig][获取 Modbus 配置失败: {}]", result); | ||
| 54 | + return new ArrayList<>(configCache.values()); | ||
| 55 | + } | ||
| 56 | + List<IotModbusDeviceConfigRespDTO> configs = result.getData(); | ||
| 57 | + | ||
| 58 | + // 2. 更新缓存 | ||
| 59 | + for (IotModbusDeviceConfigRespDTO config : configs) { | ||
| 60 | + configCache.put(config.getDeviceId(), config); | ||
| 61 | + knownDeviceIds.add(config.getDeviceId()); | ||
| 62 | + } | ||
| 63 | + return configs; | ||
| 64 | + } catch (Exception e) { | ||
| 65 | + log.error("[refreshConfig][刷新配置失败]", e); | ||
| 66 | + return new ArrayList<>(configCache.values()); | ||
| 67 | + } | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + /** | ||
| 71 | + * 获取设备配置 | ||
| 72 | + * | ||
| 73 | + * @param deviceId 设备 ID | ||
| 74 | + * @return 配置 | ||
| 75 | + */ | ||
| 76 | + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { | ||
| 77 | + return configCache.get(deviceId); | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + /** | ||
| 81 | + * 清理已删除设备的资源 | ||
| 82 | + * | ||
| 83 | + * @param currentConfigs 当前有效的配置列表 | ||
| 84 | + * @param cleanupAction 清理动作 | ||
| 85 | + */ | ||
| 86 | + public void cleanupRemovedDevices(List<IotModbusDeviceConfigRespDTO> currentConfigs, Consumer<Long> cleanupAction) { | ||
| 87 | + // 1.1 获取当前有效的设备 ID | ||
| 88 | + // TODO @AI:convertSet 简化; | ||
| 89 | + Set<Long> currentDeviceIds = new HashSet<>(); | ||
| 90 | + for (IotModbusDeviceConfigRespDTO config : currentConfigs) { | ||
| 91 | + currentDeviceIds.add(config.getDeviceId()); | ||
| 92 | + } | ||
| 93 | + // 1.2 找出已删除的设备 | ||
| 94 | + Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds); | ||
| 95 | + removedDeviceIds.removeAll(currentDeviceIds); | ||
| 96 | + | ||
| 97 | + // 2. 清理已删除设备 | ||
| 98 | + for (Long deviceId : removedDeviceIds) { | ||
| 99 | + log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); | ||
| 100 | + configCache.remove(deviceId); | ||
| 101 | + knownDeviceIds.remove(deviceId); | ||
| 102 | + cleanupAction.accept(deviceId); | ||
| 103 | + } | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | ||
| 4 | +import com.ghgande.j2mod.modbus.net.TCPMasterConnection; | ||
| 5 | +import io.vertx.core.Context; | ||
| 6 | +import io.vertx.core.Future; | ||
| 7 | +import io.vertx.core.Vertx; | ||
| 8 | +import lombok.Data; | ||
| 9 | +import lombok.RequiredArgsConstructor; | ||
| 10 | +import lombok.extern.slf4j.Slf4j; | ||
| 11 | +import org.redisson.api.RLock; | ||
| 12 | +import org.redisson.api.RedissonClient; | ||
| 13 | +import org.springframework.stereotype.Component; | ||
| 14 | + | ||
| 15 | +import java.net.InetAddress; | ||
| 16 | +import java.util.Map; | ||
| 17 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 18 | + | ||
| 19 | +/** | ||
| 20 | + * IoT Modbus TCP 连接管理器 | ||
| 21 | + * | ||
| 22 | + * 负责: | ||
| 23 | + * 1. 管理 TCP 连接(相同 ip:port 共用连接) | ||
| 24 | + * 2. 分布式锁管理(连接级别) | ||
| 25 | + * 3. 连接重试和故障恢复 | ||
| 26 | + * | ||
| 27 | + * @author 芋道源码 | ||
| 28 | + */ | ||
| 29 | +// TODO @AI:希望它的初始化,在 configuration 里; | ||
| 30 | +@Component | ||
| 31 | +@RequiredArgsConstructor | ||
| 32 | +@Slf4j | ||
| 33 | +public class IotModbusTcpConnectionManager { | ||
| 34 | + | ||
| 35 | + // TODO @AI:iot:modbus-tcp:connection: | ||
| 36 | + private static final String LOCK_KEY_PREFIX = "iot:modbus:connection:"; | ||
| 37 | + | ||
| 38 | + private final RedissonClient redissonClient; | ||
| 39 | + private final Vertx vertx; | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * 连接池:key = ip:port | ||
| 43 | + */ | ||
| 44 | + private final Map<String, ModbusConnection> connectionPool = new ConcurrentHashMap<>(); | ||
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * 设备 ID 到连接 key 的映射 | ||
| 48 | + */ | ||
| 49 | + private final Map<Long, String> deviceConnectionMap = new ConcurrentHashMap<>(); | ||
| 50 | + | ||
| 51 | + /** | ||
| 52 | + * 确保连接存在 | ||
| 53 | + * | ||
| 54 | + * @param config 设备配置 | ||
| 55 | + */ | ||
| 56 | + public void ensureConnection(IotModbusDeviceConfigRespDTO config) { | ||
| 57 | + // 1. 记录设备与连接的关系 | ||
| 58 | + String connectionKey = buildConnectionKey(config.getIp(), config.getPort()); | ||
| 59 | + deviceConnectionMap.put(config.getDeviceId(), connectionKey); | ||
| 60 | + | ||
| 61 | + // 2. 情况一:连接已存在,添加设备引用 | ||
| 62 | + ModbusConnection connection = connectionPool.get(connectionKey); | ||
| 63 | + if (connection != null) { | ||
| 64 | + // 添加设备引用 | ||
| 65 | + connection.addDevice(config.getDeviceId(), config.getSlaveId()); | ||
| 66 | + // 更新连接参数(取最小值) | ||
| 67 | + // TODO @AI:(不确定)如果后续最小值被移除后,是不是无法灰度到上一个最小值? | ||
| 68 | + connection.updateParams(config.getTimeout(), config.getRetryInterval()); | ||
| 69 | + return; | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + // 3. 情况二:连接不存在,创建新连接 | ||
| 73 | + // 3.1 尝试获取分布式锁 | ||
| 74 | + RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + connectionKey); | ||
| 75 | + if (!lock.tryLock()) { | ||
| 76 | + log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey); | ||
| 77 | + return; | ||
| 78 | + } | ||
| 79 | + // 3.2 创建新连接 | ||
| 80 | + try { | ||
| 81 | + connection = createConnection(config, lock); | ||
| 82 | + connectionPool.put(connectionKey, connection); | ||
| 83 | + log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey); | ||
| 84 | + } catch (Exception e) { | ||
| 85 | + log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e); | ||
| 86 | + lock.unlock(); | ||
| 87 | + } | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + /** | ||
| 91 | + * 创建 Modbus TCP 连接 | ||
| 92 | + */ | ||
| 93 | + private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config, RLock lock) throws Exception { | ||
| 94 | + // 创建 TCP 连接 | ||
| 95 | + // TODO @AI:需要重连么? | ||
| 96 | + TCPMasterConnection tcpConnection = new TCPMasterConnection(InetAddress.getByName(config.getIp())); | ||
| 97 | + tcpConnection.setPort(config.getPort()); | ||
| 98 | + tcpConnection.setTimeout(config.getTimeout()); | ||
| 99 | + tcpConnection.connect(); | ||
| 100 | + | ||
| 101 | + // 创建 Modbus 连接对象 | ||
| 102 | + ModbusConnection connection = new ModbusConnection(); | ||
| 103 | + // TODO @AI:链式调用,简化下; | ||
| 104 | + connection.setConnectionKey(buildConnectionKey(config.getIp(), config.getPort())); | ||
| 105 | + connection.setTcpConnection(tcpConnection); | ||
| 106 | + connection.setLock(lock); | ||
| 107 | + connection.setTimeout(config.getTimeout()); | ||
| 108 | + connection.setRetryInterval(config.getRetryInterval()); | ||
| 109 | + connection.setContext(vertx.getOrCreateContext()); | ||
| 110 | + connection.addDevice(config.getDeviceId(), config.getSlaveId()); | ||
| 111 | + return connection; | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + /** | ||
| 115 | + * 获取连接 | ||
| 116 | + */ | ||
| 117 | + public ModbusConnection getConnection(Long deviceId) { | ||
| 118 | + String connectionKey = deviceConnectionMap.get(deviceId); | ||
| 119 | + if (connectionKey == null) { | ||
| 120 | + return null; | ||
| 121 | + } | ||
| 122 | + return connectionPool.get(connectionKey); | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + /** | ||
| 126 | + * 获取设备的 slave ID | ||
| 127 | + */ | ||
| 128 | + public Integer getSlaveId(Long deviceId) { | ||
| 129 | + ModbusConnection connection = getConnection(deviceId); | ||
| 130 | + if (connection == null) { | ||
| 131 | + return null; | ||
| 132 | + } | ||
| 133 | + return connection.getSlaveId(deviceId); | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + /** | ||
| 137 | + * 移除设备 | ||
| 138 | + */ | ||
| 139 | + public void removeDevice(Long deviceId) { | ||
| 140 | + // 1. 移除设备引用 | ||
| 141 | + String connectionKey = deviceConnectionMap.remove(deviceId); | ||
| 142 | + if (connectionKey == null) { | ||
| 143 | + return; | ||
| 144 | + } | ||
| 145 | + ModbusConnection connection = connectionPool.get(connectionKey); | ||
| 146 | + if (connection == null) { | ||
| 147 | + return; | ||
| 148 | + } | ||
| 149 | + connection.removeDevice(deviceId); | ||
| 150 | + | ||
| 151 | + // 2. 如果没有设备引用了,关闭连接 | ||
| 152 | + if (connection.getDeviceCount() == 0) { | ||
| 153 | + closeConnection(connectionKey); | ||
| 154 | + } | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + /** | ||
| 158 | + * 关闭指定连接 | ||
| 159 | + */ | ||
| 160 | + private void closeConnection(String connectionKey) { | ||
| 161 | + ModbusConnection connection = connectionPool.remove(connectionKey); | ||
| 162 | + if (connection == null) { | ||
| 163 | + return; | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + try { | ||
| 167 | + if (connection.getTcpConnection() != null) { | ||
| 168 | + connection.getTcpConnection().close(); | ||
| 169 | + } | ||
| 170 | + // TODO @AI:(不确定)是不是要当前线程?还是当前进程就 ok 了。 | ||
| 171 | + if (connection.getLock() != null && connection.getLock().isHeldByCurrentThread()) { | ||
| 172 | + connection.getLock().unlock(); | ||
| 173 | + } | ||
| 174 | + log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey); | ||
| 175 | + } catch (Exception e) { | ||
| 176 | + log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e); | ||
| 177 | + } | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + /** | ||
| 181 | + * 关闭所有连接 | ||
| 182 | + */ | ||
| 183 | + public void closeAll() { | ||
| 184 | + for (String connectionKey : connectionPool.keySet()) { | ||
| 185 | + closeConnection(connectionKey); | ||
| 186 | + } | ||
| 187 | + deviceConnectionMap.clear(); | ||
| 188 | + } | ||
| 189 | + | ||
| 190 | + private String buildConnectionKey(String ip, Integer port) { | ||
| 191 | + return ip + ":" + port; | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + /** | ||
| 195 | + * Modbus 连接信息 | ||
| 196 | + */ | ||
| 197 | + @Data | ||
| 198 | + public static class ModbusConnection { | ||
| 199 | + | ||
| 200 | + private String connectionKey; | ||
| 201 | + private TCPMasterConnection tcpConnection; | ||
| 202 | + private RLock lock; | ||
| 203 | + private Integer timeout; | ||
| 204 | + private Integer retryInterval; | ||
| 205 | + private Context context; | ||
| 206 | + | ||
| 207 | + /** | ||
| 208 | + * 设备 ID 到 slave ID 的映射 | ||
| 209 | + */ | ||
| 210 | + private final Map<Long, Integer> deviceSlaveMap = new ConcurrentHashMap<>(); | ||
| 211 | + | ||
| 212 | + public void addDevice(Long deviceId, Integer slaveId) { | ||
| 213 | + deviceSlaveMap.put(deviceId, slaveId); | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + public void removeDevice(Long deviceId) { | ||
| 217 | + deviceSlaveMap.remove(deviceId); | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + public int getDeviceCount() { | ||
| 221 | + return deviceSlaveMap.size(); | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + public Integer getSlaveId(Long deviceId) { | ||
| 225 | + return deviceSlaveMap.get(deviceId); | ||
| 226 | + } | ||
| 227 | + | ||
| 228 | + public void updateParams(Integer timeout, Integer retryInterval) { | ||
| 229 | + // 取最小值 | ||
| 230 | + if (timeout != null && (this.timeout == null || timeout < this.timeout)) { | ||
| 231 | + this.timeout = timeout; | ||
| 232 | + } | ||
| 233 | + if (retryInterval != null && (this.retryInterval == null || retryInterval < this.retryInterval)) { | ||
| 234 | + this.retryInterval = retryInterval; | ||
| 235 | + } | ||
| 236 | + } | ||
| 237 | + | ||
| 238 | + /** | ||
| 239 | + * 执行 Modbus 读取操作(阻塞方式,在 Vert.x worker 线程执行) | ||
| 240 | + */ | ||
| 241 | + public <T> Future<T> executeBlocking(java.util.function.Function<TCPMasterConnection, T> operation) { | ||
| 242 | + // TODO @AI:executeBlocking 方法,已经废弃了,看看要不要替换。 | ||
| 243 | + // TODO @AI:疑问。这里会不会线程阻塞?多个设备之间,担心性能; | ||
| 244 | + return context.executeBlocking(promise -> { | ||
| 245 | + try { | ||
| 246 | + T result = operation.apply(tcpConnection); | ||
| 247 | + promise.complete(result); | ||
| 248 | + } catch (Exception e) { | ||
| 249 | + promise.fail(e); | ||
| 250 | + } | ||
| 251 | + }, true); // ordered=true 保证同一连接串行执行 | ||
| 252 | + } | ||
| 253 | + } | ||
| 254 | + | ||
| 255 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | ||
| 5 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 6 | +import lombok.RequiredArgsConstructor; | ||
| 7 | +import lombok.extern.slf4j.Slf4j; | ||
| 8 | +import org.springframework.stereotype.Component; | ||
| 9 | + | ||
| 10 | +import java.util.Map; | ||
| 11 | + | ||
| 12 | +// TODO @AI:看看注释能不能优化下; | ||
| 13 | +/** | ||
| 14 | + * IoT Modbus TCP 下行消息处理器 | ||
| 15 | + * | ||
| 16 | + * 负责: | ||
| 17 | + * 1. 处理属性设置消息(thing.service.property.set) | ||
| 18 | + * 2. 将属性值转换为 Modbus 原始值 | ||
| 19 | + * 3. 执行 Modbus 写入操作 | ||
| 20 | + * | ||
| 21 | + * @author 芋道源码 | ||
| 22 | + */ | ||
| 23 | +// TODO @AI:希望它的初始化,在 configuration 里; | ||
| 24 | +@Component | ||
| 25 | +@RequiredArgsConstructor | ||
| 26 | +@Slf4j | ||
| 27 | +public class IotModbusTcpDownstreamHandler { | ||
| 28 | + | ||
| 29 | + private final IotModbusTcpConnectionManager connectionManager; | ||
| 30 | + private final IotModbusTcpClient modbusClient; | ||
| 31 | + private final IotModbusDataConverter dataConverter; | ||
| 32 | + private final IotModbusTcpConfigCacheService configCacheService; | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 处理下行消息 | ||
| 36 | + */ | ||
| 37 | + @SuppressWarnings("unchecked") | ||
| 38 | + public void handle(IotDeviceMessage message) { | ||
| 39 | + // 1.1 检查是否是属性设置消息 | ||
| 40 | + if (!"thing.service.property.set".equals(message.getMethod())) { | ||
| 41 | + log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); | ||
| 42 | + return; | ||
| 43 | + } | ||
| 44 | + // 1.2 获取设备配置 | ||
| 45 | + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId()); | ||
| 46 | + if (config == null) { | ||
| 47 | + log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId()); | ||
| 48 | + return; | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + // 2. 解析属性值并写入 | ||
| 52 | + Object params = message.getParams(); | ||
| 53 | + if (!(params instanceof Map)) { | ||
| 54 | + log.warn("[handle][params 不是 Map 类型: {}]", params); | ||
| 55 | + return; | ||
| 56 | + } | ||
| 57 | + Map<String, Object> propertyMap = (Map<String, Object>) params; | ||
| 58 | + for (Map.Entry<String, Object> entry : propertyMap.entrySet()) { | ||
| 59 | + String identifier = entry.getKey(); | ||
| 60 | + Object value = entry.getValue(); | ||
| 61 | + // 2.1.1 查找对应的点位配置 | ||
| 62 | + IotModbusPointRespDTO point = findPoint(config, identifier); | ||
| 63 | + if (point == null) { | ||
| 64 | + log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); | ||
| 65 | + continue; | ||
| 66 | + } | ||
| 67 | + // 2.1.2 检查是否支持写操作 | ||
| 68 | + if (!isWritable(point.getFunctionCode())) { | ||
| 69 | + log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); | ||
| 70 | + continue; | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + // 2.2 执行写入 | ||
| 74 | + writeProperty(config, point, value); | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + /** | ||
| 79 | + * 写入属性值 | ||
| 80 | + */ | ||
| 81 | + private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) { | ||
| 82 | + // 1.1 获取连接 | ||
| 83 | + IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); | ||
| 84 | + if (connection == null) { | ||
| 85 | + log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId()); | ||
| 86 | + return; | ||
| 87 | + } | ||
| 88 | + // 1.2 获取 slave ID | ||
| 89 | + Integer slaveId = connectionManager.getSlaveId(config.getDeviceId()); | ||
| 90 | + if (slaveId == null) { | ||
| 91 | + log.warn("[writeProperty][设备 {} 没有 slaveId]", config.getDeviceId()); | ||
| 92 | + return; | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + // 2.1 转换属性值为原始值 | ||
| 96 | + int[] rawValues = dataConverter.convertToRawValues(value, point); | ||
| 97 | + // 2.2 执行 Modbus 写入 | ||
| 98 | + modbusClient.write(connection, slaveId, point, rawValues) | ||
| 99 | + .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", | ||
| 100 | + config.getDeviceId(), point.getIdentifier(), value)) | ||
| 101 | + .onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]", | ||
| 102 | + config.getDeviceId(), point.getIdentifier(), e)); | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + /** | ||
| 106 | + * 查找点位配置 | ||
| 107 | + */ | ||
| 108 | + private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { | ||
| 109 | + if (config.getPoints() == null) { | ||
| 110 | + return null; | ||
| 111 | + } | ||
| 112 | + // TODO @AI:hutool findOne? | ||
| 113 | + return config.getPoints().stream() | ||
| 114 | + .filter(p -> identifier.equals(p.getIdentifier())) | ||
| 115 | + .findFirst() | ||
| 116 | + .orElse(null); | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + /** | ||
| 120 | + * 检查功能码是否支持写操作 | ||
| 121 | + */ | ||
| 122 | + private boolean isWritable(Integer functionCode) { | ||
| 123 | + // TODO @AI:能不能通过 枚举优化下? | ||
| 124 | + // 功能码 1(ReadCoils)和 3(ReadHoldingRegisters)支持写操作 | ||
| 125 | + return functionCode != null && (functionCode == 1 || functionCode == 3); | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | ||
| 5 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 6 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | ||
| 7 | +import jakarta.annotation.PostConstruct; | ||
| 8 | +import lombok.RequiredArgsConstructor; | ||
| 9 | +import lombok.extern.slf4j.Slf4j; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * IoT Modbus TCP 下行消息订阅器 | ||
| 13 | + * | ||
| 14 | + * 负责: | ||
| 15 | + * 1. 订阅消息总线的下行消息 | ||
| 16 | + * 2. 将属性设置消息转发给下行处理器 | ||
| 17 | + * | ||
| 18 | + * @author 芋道源码 | ||
| 19 | + */ | ||
| 20 | +// TODO @AI:希望它的初始化,在 configuration 里; | ||
| 21 | +@RequiredArgsConstructor | ||
| 22 | +@Slf4j | ||
| 23 | +public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | ||
| 24 | + | ||
| 25 | + private final IotModbusTcpUpstreamProtocol upstreamProtocol; | ||
| 26 | + private final IotModbusTcpDownstreamHandler downstreamHandler; | ||
| 27 | + private final IotMessageBus messageBus; | ||
| 28 | + | ||
| 29 | + @PostConstruct | ||
| 30 | + public void subscribe() { | ||
| 31 | + messageBus.register(this); | ||
| 32 | + log.info("[subscribe][Modbus TCP 下行消息订阅器已启动, topic={}]", getTopic()); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + @Override | ||
| 36 | + public String getTopic() { | ||
| 37 | + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId()); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + @Override | ||
| 41 | + public String getGroup() { | ||
| 42 | + return getTopic(); // 点对点消费 | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + @Override | ||
| 46 | + public void onMessage(IotDeviceMessage message) { | ||
| 47 | + log.debug("[onMessage][收到下行消息: {}]", message); | ||
| 48 | + try { | ||
| 49 | + downstreamHandler.handle(message); | ||
| 50 | + } catch (Exception e) { | ||
| 51 | + log.error("[onMessage][处理下行消息失败]", e); | ||
| 52 | + } | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | ||
| 5 | +import io.vertx.core.Vertx; | ||
| 6 | +import lombok.RequiredArgsConstructor; | ||
| 7 | +import lombok.extern.slf4j.Slf4j; | ||
| 8 | +import org.springframework.stereotype.Component; | ||
| 9 | + | ||
| 10 | +import java.util.ArrayList; | ||
| 11 | +import java.util.List; | ||
| 12 | +import java.util.Map; | ||
| 13 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 14 | + | ||
| 15 | +/** | ||
| 16 | + * IoT Modbus TCP 轮询调度器 | ||
| 17 | + * | ||
| 18 | + * 负责: | ||
| 19 | + * 1. 管理每个点位的轮询定时器 | ||
| 20 | + * 2. 调度 Modbus 读取任务 | ||
| 21 | + * 3. 处理读取结果并上报 | ||
| 22 | + * | ||
| 23 | + * @author 芋道源码 | ||
| 24 | + */ | ||
| 25 | +// TODO @AI:希望它的初始化,在 configuration 里; | ||
| 26 | +@Component | ||
| 27 | +@RequiredArgsConstructor | ||
| 28 | +@Slf4j | ||
| 29 | +public class IotModbusTcpPollScheduler { | ||
| 30 | + | ||
| 31 | + private final Vertx vertx; | ||
| 32 | + private final IotModbusTcpConnectionManager connectionManager; | ||
| 33 | + private final IotModbusTcpClient modbusClient; | ||
| 34 | + private final IotModbusTcpUpstreamHandler upstreamHandler; | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * 设备的定时器列表:deviceId -> timerId 列表 | ||
| 38 | + */ | ||
| 39 | + private final Map<Long, List<Long>> deviceTimers = new ConcurrentHashMap<>(); | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * 更新轮询任务 | ||
| 43 | + */ | ||
| 44 | + public void updatePolling(IotModbusDeviceConfigRespDTO config) { | ||
| 45 | + Long deviceId = config.getDeviceId(); | ||
| 46 | + | ||
| 47 | + // 1. 停止旧的轮询任务 | ||
| 48 | + stopPolling(deviceId); | ||
| 49 | + | ||
| 50 | + // 2.1 为每个点位创建新的轮询任务 | ||
| 51 | + List<Long> timerIds = new ArrayList<>(); | ||
| 52 | + // TODO @AI:if return 简化;上面的 size 加下;config.getPoints() | ||
| 53 | + if (config.getPoints() != null) { | ||
| 54 | + for (IotModbusPointRespDTO point : config.getPoints()) { | ||
| 55 | + Long timerId = createPollTimer(config, point); | ||
| 56 | + if (timerId != null) { | ||
| 57 | + timerIds.add(timerId); | ||
| 58 | + } | ||
| 59 | + } | ||
| 60 | + } | ||
| 61 | + // 2.2 记录定时器 | ||
| 62 | + // TODO @AI:CollUtil.isNotEmpty;if return 简化; | ||
| 63 | + if (!timerIds.isEmpty()) { | ||
| 64 | + deviceTimers.put(deviceId, timerIds); | ||
| 65 | + log.debug("[updatePolling][设备 {} 创建了 {} 个轮询定时器]", deviceId, timerIds.size()); | ||
| 66 | + } | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + /** | ||
| 70 | + * 创建轮询定时器 | ||
| 71 | + */ | ||
| 72 | + private Long createPollTimer(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { | ||
| 73 | + if (point.getPollInterval() == null || point.getPollInterval() <= 0) { | ||
| 74 | + return null; | ||
| 75 | + } | ||
| 76 | + return vertx.setPeriodic(point.getPollInterval(), timerId -> { | ||
| 77 | + try { | ||
| 78 | + pollPoint(config, point); | ||
| 79 | + } catch (Exception e) { | ||
| 80 | + log.error("[createPollTimer][轮询点位失败, deviceId={}, identifier={}]", | ||
| 81 | + config.getDeviceId(), point.getIdentifier(), e); | ||
| 82 | + } | ||
| 83 | + }); | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + /** | ||
| 87 | + * 轮询单个点位 | ||
| 88 | + */ | ||
| 89 | + private void pollPoint(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { | ||
| 90 | + // 1.1 获取连接 | ||
| 91 | + IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); | ||
| 92 | + if (connection == null) { | ||
| 93 | + log.warn("[pollPoint][设备 {} 没有连接]", config.getDeviceId()); | ||
| 94 | + return; | ||
| 95 | + } | ||
| 96 | + // 1.2 获取 slave ID | ||
| 97 | + Integer slaveId = connectionManager.getSlaveId(config.getDeviceId()); | ||
| 98 | + if (slaveId == null) { | ||
| 99 | + log.warn("[pollPoint][设备 {} 没有 slaveId]", config.getDeviceId()); | ||
| 100 | + return; | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + // 2. 执行 Modbus 读取 | ||
| 104 | + // TODO @AI:超时时间,有实现么??? | ||
| 105 | + modbusClient.read(connection, slaveId, point) | ||
| 106 | + .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue)) | ||
| 107 | + .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]", | ||
| 108 | + config.getDeviceId(), point.getIdentifier(), e)); | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + /** | ||
| 112 | + * 停止设备的轮询 | ||
| 113 | + */ | ||
| 114 | + public void stopPolling(Long deviceId) { | ||
| 115 | + List<Long> timerIds = deviceTimers.remove(deviceId); | ||
| 116 | + // TODO @AI:CollUtil.isNotEmpty;并且 if return; | ||
| 117 | + if (timerIds != null) { | ||
| 118 | + for (Long timerId : timerIds) { | ||
| 119 | + vertx.cancelTimer(timerId); | ||
| 120 | + } | ||
| 121 | + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timerIds.size()); | ||
| 122 | + } | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + /** | ||
| 126 | + * 停止所有轮询 | ||
| 127 | + */ | ||
| 128 | + public void stopAll() { | ||
| 129 | + for (Long deviceId : deviceTimers.keySet()) { | ||
| 130 | + stopPolling(deviceId); | ||
| 131 | + } | ||
| 132 | + } | ||
| 133 | + | ||
| 134 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | ||
| 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 lombok.extern.slf4j.Slf4j; | ||
| 8 | +import org.springframework.stereotype.Component; | ||
| 9 | + | ||
| 10 | +import java.util.HashMap; | ||
| 11 | +import java.util.Map; | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * IoT Modbus TCP 上行数据处理器 | ||
| 15 | + * | ||
| 16 | + * 负责: | ||
| 17 | + * 1. 将 Modbus 读取的原始值转换为物模型属性值 | ||
| 18 | + * 2. 构造属性上报消息 | ||
| 19 | + * 3. 发送消息到消息总线 | ||
| 20 | + * | ||
| 21 | + * @author 芋道源码 | ||
| 22 | + */ | ||
| 23 | +// TODO @AI:希望它的初始化,在 configuration 里; | ||
| 24 | +@Component | ||
| 25 | +@Slf4j | ||
| 26 | +public class IotModbusTcpUpstreamHandler { | ||
| 27 | + | ||
| 28 | + private final IotDeviceMessageService messageService; | ||
| 29 | + private final IotModbusDataConverter dataConverter; | ||
| 30 | + | ||
| 31 | + private String serverId; | ||
| 32 | + | ||
| 33 | + public IotModbusTcpUpstreamHandler(IotDeviceMessageService messageService, | ||
| 34 | + IotModbusDataConverter dataConverter) { | ||
| 35 | + this.messageService = messageService; | ||
| 36 | + this.dataConverter = dataConverter; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + // TODO @AI:lombok 简化 | ||
| 40 | + /** | ||
| 41 | + * 设置 serverId | ||
| 42 | + * | ||
| 43 | + * @param serverId 服务器 ID | ||
| 44 | + */ | ||
| 45 | + public void setServerId(String serverId) { | ||
| 46 | + this.serverId = serverId; | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + /** | ||
| 50 | + * 处理 Modbus 读取结果 | ||
| 51 | + * | ||
| 52 | + * @param config 设备配置 | ||
| 53 | + * @param point 点位配置 | ||
| 54 | + * @param rawValue 原始值(int 数组) | ||
| 55 | + */ | ||
| 56 | + public void handleReadResult(IotModbusDeviceConfigRespDTO config, | ||
| 57 | + IotModbusPointRespDTO point, | ||
| 58 | + int[] rawValue) { | ||
| 59 | + try { | ||
| 60 | + // 1.1 转换原始值为物模型属性值 | ||
| 61 | + Object convertedValue = dataConverter.convertToPropertyValue(rawValue, point); | ||
| 62 | + log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]", | ||
| 63 | + config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue); | ||
| 64 | + // 1.2 构造属性上报消息 | ||
| 65 | + Map<String, Object> params = new HashMap<>(); | ||
| 66 | + params.put(point.getIdentifier(), convertedValue); | ||
| 67 | + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.property.post", params); | ||
| 68 | + | ||
| 69 | + // 2. 发送到消息总线 | ||
| 70 | + messageService.sendDeviceMessage(message, config.getProductKey(), | ||
| 71 | + config.getDeviceName(), serverId); | ||
| 72 | + } catch (Exception e) { | ||
| 73 | + log.error("[handleReadResult][处理读取结果失败, deviceId={}, identifier={}]", | ||
| 74 | + config.getDeviceId(), point.getIdentifier(), e); | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | ||
| 4 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | ||
| 5 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 6 | +import io.vertx.core.Vertx; | ||
| 7 | +import jakarta.annotation.PostConstruct; | ||
| 8 | +import jakarta.annotation.PreDestroy; | ||
| 9 | +import lombok.RequiredArgsConstructor; | ||
| 10 | +import lombok.extern.slf4j.Slf4j; | ||
| 11 | + | ||
| 12 | +import java.util.List; | ||
| 13 | +import java.util.UUID; | ||
| 14 | +import java.util.concurrent.TimeUnit; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * IoT Modbus TCP 上行协议 | ||
| 18 | + * | ||
| 19 | + * 负责: | ||
| 20 | + * 1. 定时从 biz 拉取 Modbus 设备配置 | ||
| 21 | + * 2. 管理 Modbus TCP 连接 | ||
| 22 | + * 3. 调度轮询任务 | ||
| 23 | + * 4. 处理采集数据上报 | ||
| 24 | + * | ||
| 25 | + * @author 芋道源码 | ||
| 26 | + */ | ||
| 27 | +// TODO @AI:希望它的初始化,在 configuration 里; | ||
| 28 | +@RequiredArgsConstructor | ||
| 29 | +@Slf4j | ||
| 30 | +public class IotModbusTcpUpstreamProtocol { | ||
| 31 | + | ||
| 32 | + private final IotGatewayProperties.ModbusTcpProperties modbusTcpProperties; | ||
| 33 | + private final IotDeviceMessageService messageService; | ||
| 34 | + private final IotModbusTcpConnectionManager connectionManager; | ||
| 35 | + private final IotModbusTcpPollScheduler pollScheduler; | ||
| 36 | + private final IotModbusTcpConfigCacheService configCacheService; | ||
| 37 | + private final IotModbusTcpUpstreamHandler upstreamHandler; | ||
| 38 | + private final Vertx vertx; | ||
| 39 | + | ||
| 40 | + // TODO @AI:按照别的模块,生成 serverId; | ||
| 41 | + /** | ||
| 42 | + * 服务器 ID,用于标识当前网关实例 | ||
| 43 | + */ | ||
| 44 | + private final String serverId = UUID.randomUUID().toString(); | ||
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * 配置刷新定时器 ID | ||
| 48 | + */ | ||
| 49 | + private Long configRefreshTimerId; | ||
| 50 | + | ||
| 51 | + @PostConstruct | ||
| 52 | + public void start() { | ||
| 53 | + log.info("[start][Modbus TCP 协议启动, serverId={}]", serverId); | ||
| 54 | + | ||
| 55 | + // 0. 设置 serverId 到上行处理器 | ||
| 56 | + upstreamHandler.setServerId(serverId); | ||
| 57 | + | ||
| 58 | + // 1. 首次加载配置 | ||
| 59 | + refreshConfig(); | ||
| 60 | + | ||
| 61 | + // 2. 启动配置定时刷新 | ||
| 62 | + int refreshInterval = modbusTcpProperties.getConfigRefreshInterval(); | ||
| 63 | + configRefreshTimerId = vertx.setPeriodic( | ||
| 64 | + TimeUnit.SECONDS.toMillis(refreshInterval), | ||
| 65 | + id -> refreshConfig() | ||
| 66 | + ); | ||
| 67 | + log.info("[start][配置刷新定时器已启动, 间隔={}秒]", refreshInterval); | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + @PreDestroy | ||
| 71 | + public void stop() { | ||
| 72 | + log.info("[stop][Modbus TCP 协议停止]"); | ||
| 73 | + | ||
| 74 | + // 1. 取消配置刷新定时器 | ||
| 75 | + if (configRefreshTimerId != null) { | ||
| 76 | + vertx.cancelTimer(configRefreshTimerId); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + // 2. 停止轮询调度器 | ||
| 80 | + pollScheduler.stopAll(); | ||
| 81 | + | ||
| 82 | + // 3. 关闭所有连接 | ||
| 83 | + connectionManager.closeAll(); | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + /** | ||
| 87 | + * 刷新配置 | ||
| 88 | + */ | ||
| 89 | + private void refreshConfig() { | ||
| 90 | + try { | ||
| 91 | + // 1. 从 biz 拉取最新配置 | ||
| 92 | + List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig(); | ||
| 93 | + log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); | ||
| 94 | + | ||
| 95 | + // 2. 更新连接和轮询任务 | ||
| 96 | + for (IotModbusDeviceConfigRespDTO config : configs) { | ||
| 97 | + try { | ||
| 98 | + // 2.1 确保连接存在 | ||
| 99 | + connectionManager.ensureConnection(config); | ||
| 100 | + // 2.2 更新轮询任务 | ||
| 101 | + // TODO @AI:【重要】如果点位配置没变化,是不是不用 update? | ||
| 102 | + pollScheduler.updatePolling(config); | ||
| 103 | + } catch (Exception e) { | ||
| 104 | + log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); | ||
| 105 | + } | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + // 3. 清理已删除设备的资源 | ||
| 109 | + configCacheService.cleanupRemovedDevices(configs, deviceId -> { | ||
| 110 | + pollScheduler.stopPolling(deviceId); | ||
| 111 | + connectionManager.removeDevice(deviceId); | ||
| 112 | + }); | ||
| 113 | + } catch (Exception e) { | ||
| 114 | + log.error("[refreshConfig][刷新配置失败]", e); | ||
| 115 | + } | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + // TODO @AI:是不是 lombok 简化 | ||
| 119 | + public String getServerId() { | ||
| 120 | + return serverId; | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + // TODO @AI:不需要;可以删除; | ||
| 124 | + public IotDeviceMessageService getMessageService() { | ||
| 125 | + return messageService; | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java
| @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 7 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; | 7 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; |
| 8 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | 8 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 9 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | ||
| 9 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | 10 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 10 | import jakarta.annotation.PostConstruct; | 11 | import jakarta.annotation.PostConstruct; |
| 11 | import jakarta.annotation.Resource; | 12 | import jakarta.annotation.Resource; |
| @@ -18,6 +19,8 @@ import org.springframework.http.ResponseEntity; | @@ -18,6 +19,8 @@ import org.springframework.http.ResponseEntity; | ||
| 18 | import org.springframework.stereotype.Service; | 19 | import org.springframework.stereotype.Service; |
| 19 | import org.springframework.web.client.RestTemplate; | 20 | import org.springframework.web.client.RestTemplate; |
| 20 | 21 | ||
| 22 | +import java.util.List; | ||
| 23 | + | ||
| 21 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | 24 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; |
| 22 | 25 | ||
| 23 | /** | 26 | /** |
| @@ -33,6 +36,7 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { | @@ -33,6 +36,7 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { | ||
| 33 | private IotGatewayProperties gatewayProperties; | 36 | private IotGatewayProperties gatewayProperties; |
| 34 | 37 | ||
| 35 | private RestTemplate restTemplate; | 38 | private RestTemplate restTemplate; |
| 39 | + private RestTemplate modbusRestTemplate; | ||
| 36 | 40 | ||
| 37 | @PostConstruct | 41 | @PostConstruct |
| 38 | public void init() { | 42 | public void init() { |
| @@ -42,24 +46,35 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { | @@ -42,24 +46,35 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { | ||
| 42 | .readTimeout(rpc.getReadTimeout()) | 46 | .readTimeout(rpc.getReadTimeout()) |
| 43 | .connectTimeout(rpc.getConnectTimeout()) | 47 | .connectTimeout(rpc.getConnectTimeout()) |
| 44 | .build(); | 48 | .build(); |
| 49 | + // TODO @AI:继续复用 rpc.getConnectTimeout();不需要独立 modbusRestTemplate | ||
| 50 | + modbusRestTemplate = new RestTemplateBuilder() | ||
| 51 | + .rootUri(rpc.getUrl() + "/rpc-api/iot/modbus") | ||
| 52 | + .readTimeout(rpc.getReadTimeout()) | ||
| 53 | + .connectTimeout(rpc.getConnectTimeout()) | ||
| 54 | + .build(); | ||
| 45 | } | 55 | } |
| 46 | 56 | ||
| 47 | @Override | 57 | @Override |
| 48 | public CommonResult<Boolean> authDevice(IotDeviceAuthReqDTO authReqDTO) { | 58 | public CommonResult<Boolean> authDevice(IotDeviceAuthReqDTO authReqDTO) { |
| 49 | - return doPost("/auth", authReqDTO, new ParameterizedTypeReference<>() { }); | 59 | + return doPost(restTemplate, "/auth", authReqDTO, new ParameterizedTypeReference<>() { }); |
| 50 | } | 60 | } |
| 51 | 61 | ||
| 52 | @Override | 62 | @Override |
| 53 | public CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO getReqDTO) { | 63 | public CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO getReqDTO) { |
| 54 | - return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { }); | 64 | + return doPost(restTemplate, "/get", getReqDTO, new ParameterizedTypeReference<>() { }); |
| 65 | + } | ||
| 66 | + | ||
| 67 | + @Override | ||
| 68 | + public CommonResult<List<IotModbusDeviceConfigRespDTO>> getEnabledModbusDeviceConfigs() { | ||
| 69 | + return doPost(modbusRestTemplate, "/enabled-configs", null, new ParameterizedTypeReference<>() { }); | ||
| 55 | } | 70 | } |
| 56 | 71 | ||
| 57 | - private <T, R> CommonResult<R> doPost(String url, T body, | 72 | + private <T, R> CommonResult<R> doPost(RestTemplate template, String url, T body, |
| 58 | ParameterizedTypeReference<CommonResult<R>> responseType) { | 73 | ParameterizedTypeReference<CommonResult<R>> responseType) { |
| 59 | try { | 74 | try { |
| 60 | // 请求 | 75 | // 请求 |
| 61 | HttpEntity<T> requestEntity = new HttpEntity<>(body); | 76 | HttpEntity<T> requestEntity = new HttpEntity<>(body); |
| 62 | - ResponseEntity<CommonResult<R>> response = restTemplate.exchange( | 77 | + ResponseEntity<CommonResult<R>> response = template.exchange( |
| 63 | url, HttpMethod.POST, requestEntity, responseType); | 78 | url, HttpMethod.POST, requestEntity, responseType); |
| 64 | // 响应 | 79 | // 响应 |
| 65 | CommonResult<R> result = response.getBody(); | 80 | CommonResult<R> result = response.getBody(); |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
| @@ -117,6 +117,9 @@ yudao: | @@ -117,6 +117,9 @@ yudao: | ||
| 117 | keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒) | 117 | keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒) |
| 118 | ssl-enabled: false # 是否启用 SSL(wss://) | 118 | ssl-enabled: false # 是否启用 SSL(wss://) |
| 119 | sub-protocol: mqtt # WebSocket 子协议 | 119 | sub-protocol: mqtt # WebSocket 子协议 |
| 120 | + # TODO @AI:modbus-tcp 组件配置待补充 | ||
| 121 | + modbus-tcp: | ||
| 122 | + enabled: true | ||
| 120 | 123 | ||
| 121 | --- #################### 日志相关配置 #################### | 124 | --- #################### 日志相关配置 #################### |
| 122 | 125 |