diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java index eb55b18..5a01518 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java @@ -4,11 +4,13 @@ import cn.iocoder.yudao.framework.common.enums.RpcConstants; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.*; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import jakarta.annotation.Resource; @@ -19,6 +21,9 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import java.util.ArrayList; +import java.util.List; + import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** @@ -35,6 +40,10 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { private IotDeviceService deviceService; @Resource private IotProductService productService; + @Resource + private IotDeviceModbusConfigService modbusConfigService; + @Resource + private IotDeviceModbusPointService modbusPointService; @Override @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth") @@ -57,4 +66,47 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { })); } + @Override + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/enabled-configs") + @PermitAll + public CommonResult> getEnabledModbusDeviceConfigs() { + // 1. 获取所有启用的 Modbus 连接配置 + List configList = modbusConfigService.getEnabledModbusConfigList(); + if (configList.isEmpty()) { + return success(new ArrayList<>()); + } + + // 2. 组装返回结果 + List result = new ArrayList<>(configList.size()); + for (IotDeviceModbusConfigDO config : configList) { + // 2.1 获取设备信息 + // TODO @AI:设备需要批量读取;(先暂时不处理) + IotDeviceDO device = deviceService.getDeviceFromCache(config.getDeviceId()); + if (device == null) { + continue; + } + + // 2.2 获取启用的点位列表 + // TODO @AI:看看是不是批量读取; + List pointList = modbusPointService.getEnabledModbusPointListByDeviceId(config.getDeviceId()); + + // 2.3 构建 DTO + IotModbusDeviceConfigRespDTO dto = new IotModbusDeviceConfigRespDTO(); + dto.setDeviceId(config.getDeviceId()); + // TODO @AI:这个 productKey、deviceName 这个字段,要不要冗余到 IotDeviceModbusConfigDO 里面?(先暂时不处理) + dto.setProductKey(device.getProductKey()); + dto.setDeviceName(device.getDeviceName()); + dto.setTenantId(device.getTenantId()); + // TODO @AI:看看 dto 的转换,能不能通过 beanutils copy + dto.setIp(config.getIp()); + dto.setPort(config.getPort()); + dto.setSlaveId(config.getSlaveId()); + dto.setTimeout(config.getTimeout()); + dto.setRetryInterval(config.getRetryInterval()); + dto.setPoints(BeanUtils.toBean(pointList, IotModbusPointRespDTO.class)); + result.add(dto); + } + return success(result); + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java new file mode 100644 index 0000000..14648ed --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 设备 Modbus 连接配置") +@RestController +@RequestMapping("/iot/device-modbus-config") +@Validated +public class IotDeviceModbusConfigController { + + @Resource + private IotDeviceModbusConfigService modbusConfigService; + + // TODO @AI:create 和 update 合并成 save 接口; + @PostMapping("/create") + @Operation(summary = "创建设备 Modbus 连接配置") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:create')") + public CommonResult createModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO createReqVO) { + return success(modbusConfigService.createModbusConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新设备 Modbus 连接配置") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:update')") + public CommonResult updateModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO updateReqVO) { + modbusConfigService.updateModbusConfig(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除设备 Modbus 连接配置") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:delete')") + public CommonResult deleteModbusConfig(@RequestParam("id") Long id) { + modbusConfigService.deleteModbusConfig(id); + return success(true); + } + + // TODO @AI:这个接口改造,支持 id 或者 deviceId;二选一查询; + @GetMapping("/get") + @Operation(summary = "获得设备 Modbus 连接配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") + public CommonResult getModbusConfig(@RequestParam("id") Long id) { + IotDeviceModbusConfigDO modbusConfig = modbusConfigService.getModbusConfig(id); + return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class)); + } + + // TODO @AI:合并到 getModbusConfig 接口里; + @GetMapping("/get-by-device-id") + @Operation(summary = "根据设备编号获得 Modbus 连接配置") + @Parameter(name = "deviceId", description = "设备编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") + public CommonResult getModbusConfigByDeviceId(@RequestParam("deviceId") Long deviceId) { + IotDeviceModbusConfigDO modbusConfig = modbusConfigService.getModbusConfigByDeviceId(deviceId); + return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得设备 Modbus 连接配置分页") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-config:query')") + public CommonResult> getModbusConfigPage(@Valid IotDeviceModbusConfigPageReqVO pageReqVO) { + PageResult pageResult = modbusConfigService.getModbusConfigPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDeviceModbusConfigRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java new file mode 100644 index 0000000..55acc24 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 设备 Modbus 点位配置") +@RestController +@RequestMapping("/iot/device-modbus-point") +@Validated +public class IotDeviceModbusPointController { + + @Resource + private IotDeviceModbusPointService modbusPointService; + + @PostMapping("/create") + @Operation(summary = "创建设备 Modbus 点位配置") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:create')") + public CommonResult createModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO createReqVO) { + return success(modbusPointService.createModbusPoint(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新设备 Modbus 点位配置") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:update')") + public CommonResult updateModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO updateReqVO) { + modbusPointService.updateModbusPoint(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除设备 Modbus 点位配置") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:delete')") + public CommonResult deleteModbusPoint(@RequestParam("id") Long id) { + modbusPointService.deleteModbusPoint(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得设备 Modbus 点位配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") + public CommonResult getModbusPoint(@RequestParam("id") Long id) { + IotDeviceModbusPointDO modbusPoint = modbusPointService.getModbusPoint(id); + return success(BeanUtils.toBean(modbusPoint, IotDeviceModbusPointRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得设备 Modbus 点位配置分页") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") + public CommonResult> getModbusPointPage(@Valid IotDeviceModbusPointPageReqVO pageReqVO) { + PageResult pageResult = modbusPointService.getModbusPointPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDeviceModbusPointRespVO.class)); + } + + // TODO @AI:应该用不上这个接口?只需要 getModbusPointPage 分页 + @GetMapping("/list-by-device-id") + @Operation(summary = "根据设备编号获得 Modbus 点位配置列表") + @Parameter(name = "deviceId", description = "设备编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:device-modbus-point:query')") + public CommonResult> getModbusPointListByDeviceId(@RequestParam("deviceId") Long deviceId) { + List list = modbusPointService.getModbusPointListByDeviceId(deviceId); + return success(BeanUtils.toBean(list, IotDeviceModbusPointRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigPageReqVO.java new file mode 100644 index 0000000..61ff79d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigPageReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +// TODO @AI:不需要分页接口; +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IotDeviceModbusConfigPageReqVO extends PageParam { + + @Schema(description = "设备编号", example = "1024") + private Long deviceId; + + @Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100") + private String ip; + + @Schema(description = "状态", example = "0") + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java new file mode 100644 index 0000000..304b37f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置 Response VO") +@Data +public class IotDeviceModbusConfigRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long deviceId; + + @Schema(description = "设备名称", example = "温湿度传感器") + private String deviceName; + + @Schema(description = "Modbus 服务器 IP 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100") + private String ip; + + @Schema(description = "Modbus 端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "502") + private Integer port; + + @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer slaveId; + + @Schema(description = "连接超时时间(毫秒)", example = "3000") + private Integer timeout; + + @Schema(description = "重试间隔(毫秒)", example = "1000") + private Integer retryInterval; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java new file mode 100644 index 0000000..155ce5f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置新增/修改 Request VO") +@Data +public class IotDeviceModbusConfigSaveReqVO { + + @Schema(description = "主键", example = "1") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "设备编号不能为空") + private Long deviceId; + + @Schema(description = "Modbus 服务器 IP 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100") + @NotEmpty(message = "Modbus 服务器 IP 地址不能为空") + private String ip; + + @Schema(description = "Modbus 端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "502") + @NotNull(message = "Modbus 端口不能为空") + private Integer port; + + @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "从站地址不能为空") + private Integer slaveId; + + @Schema(description = "连接超时时间(毫秒)", example = "3000") + private Integer timeout; + + @Schema(description = "重试间隔(毫秒)", example = "1000") + private Integer retryInterval; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java new file mode 100644 index 0000000..344e8ce --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IotDeviceModbusPointPageReqVO extends PageParam { + + @Schema(description = "设备编号", example = "1024") + private Long deviceId; + + @Schema(description = "属性标识符", example = "temperature") + private String identifier; + + @Schema(description = "属性名称", example = "温度") + private String name; + + @Schema(description = "Modbus 功能码", example = "3") + private Integer functionCode; + + @Schema(description = "状态", example = "0") + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java new file mode 100644 index 0000000..c590e3e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置 Response VO") +@Data +public class IotDeviceModbusPointRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long deviceId; + + @Schema(description = "物模型属性编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long thingModelId; + + @Schema(description = "属性标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature") + private String identifier; + + @Schema(description = "属性名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "温度") + private String name; + + @Schema(description = "Modbus 功能码", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer functionCode; + + @Schema(description = "寄存器起始地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer registerAddress; + + @Schema(description = "寄存器数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer registerCount; + + @Schema(description = "字节序", requiredMode = Schema.RequiredMode.REQUIRED, example = "AB") + private String byteOrder; + + @Schema(description = "原始数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "INT16") + private String rawDataType; + + @Schema(description = "缩放因子", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0") + private BigDecimal scale; + + @Schema(description = "轮询间隔(毫秒)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5000") + private Integer pollInterval; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java new file mode 100644 index 0000000..18aea2b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置新增/修改 Request VO") +@Data +public class IotDeviceModbusPointSaveReqVO { + + @Schema(description = "主键", example = "1") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "设备编号不能为空") + private Long deviceId; + + @Schema(description = "物模型属性编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "物模型属性编号不能为空") + private Long thingModelId; + + @Schema(description = "Modbus 功能码", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "Modbus 功能码不能为空") + private Integer functionCode; + + @Schema(description = "寄存器起始地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "寄存器起始地址不能为空") + private Integer registerAddress; + + @Schema(description = "寄存器数量", example = "1") + private Integer registerCount; + + @Schema(description = "字节序", requiredMode = Schema.RequiredMode.REQUIRED, example = "AB") + @NotEmpty(message = "字节序不能为空") + private String byteOrder; + + @Schema(description = "原始数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "INT16") + @NotEmpty(message = "原始数据类型不能为空") + private String rawDataType; + + @Schema(description = "缩放因子", example = "1.0") + private BigDecimal scale; + + @Schema(description = "轮询间隔(毫秒)", example = "5000") + private Integer pollInterval; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java new file mode 100644 index 0000000..627ec1b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * IoT 设备 Modbus 连接配置 DO + * + * @author 芋道源码 + */ +@TableName("iot_device_modbus_config") +@KeySequence("iot_device_modbus_config_seq") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceModbusConfigDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + // TODO @AI:增加 productId; + /** + * 设备编号 + * + * 关联 {@link IotDeviceDO#getId()} + */ + private Long deviceId; + + /** + * Modbus 服务器 IP 地址 + */ + private String ip; + /** + * Modbus 服务器端口 + */ + private Integer port; + /** + * 从站地址 + */ + private Integer slaveId; + /** + * 连接超时时间,单位:毫秒 + */ + private Integer timeout; + /** + * 重试间隔,单位:毫秒 + */ + private Integer retryInterval; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java new file mode 100644 index 0000000..dc0466b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.enums.device.IotModbusByteOrderEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotModbusFunctionCodeEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotModbusRawDataTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; + +/** + * IoT 设备 Modbus 点位配置 DO + * + * @author 芋道源码 + */ +@TableName("iot_device_modbus_point") +@KeySequence("iot_device_modbus_point_seq") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceModbusPointDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + // TODO @AI:增加 productId; + /** + * 设备编号 + * + * 关联 {@link IotDeviceDO#getId()} + */ + private Long deviceId; + /** + * 物模型属性编号 + * + * 关联 {@link IotThingModelDO#getId()} + */ + private Long thingModelId; + // TODO @AI:每次物模型的变更时,需要按需刷下 identifier、name 配置; + /** + * 属性标识符 + * + * 冗余 {@link IotThingModelDO#getIdentifier()} + */ + private String identifier; + /** + * 属性名称 + * + * 冗余 {@link IotThingModelDO#getName()} + */ + private String name; + + // ========== Modbus 协议配置 ========== + + /** + * Modbus 功能码 + * + * 枚举 {@link IotModbusFunctionCodeEnum} + */ + private Integer functionCode; + /** + * 寄存器起始地址 + */ + private Integer registerAddress; + /** + * 寄存器数量 + */ + private Integer registerCount; + /** + * 字节序 + * + * 枚举 {@link IotModbusByteOrderEnum} + */ + private String byteOrder; + /** + * 原始数据类型 + * + * 枚举 {@link IotModbusRawDataTypeEnum} + */ + private String rawDataType; + /** + * 缩放因子 + */ + private BigDecimal scale; + /** + * 轮询间隔(毫秒) + */ + private Integer pollInterval; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java new file mode 100644 index 0000000..c0053dd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * IoT 设备 Modbus 连接配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotDeviceModbusConfigMapper extends BaseMapperX { + + default PageResult selectPage(IotDeviceModbusConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotDeviceModbusConfigDO::getDeviceId, reqVO.getDeviceId()) + .likeIfPresent(IotDeviceModbusConfigDO::getIp, reqVO.getIp()) + .eqIfPresent(IotDeviceModbusConfigDO::getStatus, reqVO.getStatus()) + .orderByDesc(IotDeviceModbusConfigDO::getId)); + } + + default IotDeviceModbusConfigDO selectByDeviceId(Long deviceId) { + return selectOne(IotDeviceModbusConfigDO::getDeviceId, deviceId); + } + + default List selectListByStatus(Integer status) { + return selectList(IotDeviceModbusConfigDO::getStatus, status); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java new file mode 100644 index 0000000..9043dd4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * IoT 设备 Modbus 点位配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotDeviceModbusPointMapper extends BaseMapperX { + + default PageResult selectPage(IotDeviceModbusPointPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotDeviceModbusPointDO::getDeviceId, reqVO.getDeviceId()) + .likeIfPresent(IotDeviceModbusPointDO::getIdentifier, reqVO.getIdentifier()) + .likeIfPresent(IotDeviceModbusPointDO::getName, reqVO.getName()) + .eqIfPresent(IotDeviceModbusPointDO::getFunctionCode, reqVO.getFunctionCode()) + .eqIfPresent(IotDeviceModbusPointDO::getStatus, reqVO.getStatus()) + .orderByDesc(IotDeviceModbusPointDO::getId)); + } + + default List selectListByDeviceId(Long deviceId) { + return selectList(IotDeviceModbusPointDO::getDeviceId, deviceId); + } + + // TODO @AI:是不是 selectList(f1, v1, f2, v2); + default List selectListByDeviceIdAndStatus(Long deviceId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(IotDeviceModbusPointDO::getDeviceId, deviceId) + .eq(IotDeviceModbusPointDO::getStatus, status)); + } + + // TODO @AI:是不是 selectOne(f1, v1, f2, v2); + default IotDeviceModbusPointDO selectByDeviceIdAndIdentifier(Long deviceId, String identifier) { + return selectOne(new LambdaQueryWrapperX() + .eq(IotDeviceModbusPointDO::getDeviceId, deviceId) + .eq(IotDeviceModbusPointDO::getIdentifier, identifier)); + } + + // TODO @AI:是不是删除这个方法; + default void deleteByDeviceId(Long deviceId) { + delete(IotDeviceModbusPointDO::getDeviceId, deviceId); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java index d1cf60e..4ac4dd9 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java @@ -41,6 +41,14 @@ public interface ErrorCodeConstants { ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); + // ========== 设备 Modbus 配置 1-050-006-000 ========== + ErrorCode DEVICE_MODBUS_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "设备 Modbus 连接配置不存在"); + ErrorCode DEVICE_MODBUS_CONFIG_EXISTS = new ErrorCode(1_050_006_001, "设备 Modbus 连接配置已存在"); + + // ========== 设备 Modbus 点位 1-050-007-000 ========== + ErrorCode DEVICE_MODBUS_POINT_NOT_EXISTS = new ErrorCode(1_050_007_000, "设备 Modbus 点位配置不存在"); + ErrorCode DEVICE_MODBUS_POINT_EXISTS = new ErrorCode(1_050_007_001, "设备 Modbus 点位配置已存在"); + // ========== OTA 固件相关 1-050-008-000 ========== ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java new file mode 100644 index 0000000..fdd0feb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusByteOrderEnum.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT Modbus 字节序枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum IotModbusByteOrderEnum implements ArrayValuable { + + AB("AB", "大端序(16位)", 2), + BA("BA", "小端序(16位)", 2), + ABCD("ABCD", "大端序(32位)", 4), + CDAB("CDAB", "大端字交换(32位)", 4), + DCBA("DCBA", "小端序(32位)", 4), + BADC("BADC", "小端字交换(32位)", 4); + + public static final String[] ARRAYS = Arrays.stream(values()) + .map(IotModbusByteOrderEnum::getOrder) + .toArray(String[]::new); + + /** + * 字节序 + */ + private final String order; + /** + * 名称 + */ + private final String name; + /** + * 字节数 + */ + private final Integer byteCount; + + @Override + public String[] array() { + return ARRAYS; + } + + // TODO @AI:如果不需要,可以删除掉这个方法; + /** + * 根据字节序获取枚举 + * + * @param order 字节序 + * @return 枚举 + */ + public static IotModbusByteOrderEnum getByOrder(String order) { + return Arrays.stream(values()) + .filter(e -> e.getOrder().equals(order)) + .findFirst() + .orElse(null); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java new file mode 100644 index 0000000..9fcc91e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusFunctionCodeEnum.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT Modbus 功能码枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum IotModbusFunctionCodeEnum implements ArrayValuable { + + READ_COILS(1, "读线圈", true, 5, 15), + READ_DISCRETE_INPUTS(2, "读离散输入", false, null, null), + READ_HOLDING_REGISTERS(3, "读保持寄存器", true, 6, 16), + READ_INPUT_REGISTERS(4, "读输入寄存器", false, null, null); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(IotModbusFunctionCodeEnum::getCode) + .toArray(Integer[]::new); + + /** + * 功能码 + */ + private final Integer code; + /** + * 名称 + */ + private final String name; + /** + * 是否支持写操作 + */ + private final Boolean writable; + /** + * 单个写功能码 + */ + private final Integer writeSingleCode; + /** + * 多个写功能码 + */ + private final Integer writeMultipleCode; + + @Override + public Integer[] array() { + return ARRAYS; + } + + /** + * 根据功能码获取枚举 + * + * @param code 功能码 + * @return 枚举 + */ + public static IotModbusFunctionCodeEnum valueOf(Integer code) { + return Arrays.stream(values()) + .filter(e -> e.getCode().equals(code)) + .findFirst() + .orElse(null); + } + + // TODO @AI:如果用不到,可以暂时删除哈; + /** + * 获取写功能码 + * + * @param registerCount 寄存器数量 + * @return 写功能码 + */ + public Integer getWriteCode(int registerCount) { + if (!writable) { + return null; + } + return registerCount == 1 ? writeSingleCode : writeMultipleCode; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java new file mode 100644 index 0000000..97efc14 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotModbusRawDataTypeEnum.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT Modbus 原始数据类型枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum IotModbusRawDataTypeEnum implements ArrayValuable { + + INT16("INT16", "有符号 16 位整数", 1), + UINT16("UINT16", "无符号 16 位整数", 1), + INT32("INT32", "有符号 32 位整数", 2), + UINT32("UINT32", "无符号 32 位整数", 2), + FLOAT("FLOAT", "32 位浮点数", 2), + DOUBLE("DOUBLE", "64 位浮点数", 4), + BOOLEAN("BOOLEAN", "布尔值(用于线圈)", 1), + STRING("STRING", "字符串", -1); // -1 表示可变长度 + + public static final String[] ARRAYS = Arrays.stream(values()) + .map(IotModbusRawDataTypeEnum::getType) + .toArray(String[]::new); + + /** + * 数据类型 + */ + private final String type; + /** + * 名称 + */ + private final String name; + // TODO @AI:去掉 default 会好点。null 表示可变; + /** + * 默认寄存器数量(-1 表示可变) + */ + private final Integer defaultRegisterCount; + + @Override + public String[] array() { + return ARRAYS; + } + + // TODO @AI:如果不用,可以删除掉 + /** + * 根据类型获取枚举 + * + * @param type 类型 + * @return 枚举 + */ + public static IotModbusRawDataTypeEnum getByType(String type) { + return Arrays.stream(values()) + .filter(e -> e.getType().equals(type)) + .findFirst() + .orElse(null); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java new file mode 100644 index 0000000..ded475d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * IoT 设备 Modbus 连接配置 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceModbusConfigService { + + /** + * 创建设备 Modbus 连接配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO createReqVO); + + /** + * 更新设备 Modbus 连接配置 + * + * @param updateReqVO 更新信息 + */ + void updateModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO updateReqVO); + + /** + * 删除设备 Modbus 连接配置 + * + * @param id 编号 + */ + void deleteModbusConfig(Long id); + + /** + * 获得设备 Modbus 连接配置 + * + * @param id 编号 + * @return 设备 Modbus 连接配置 + */ + IotDeviceModbusConfigDO getModbusConfig(Long id); + + /** + * 根据设备编号获得 Modbus 连接配置 + * + * @param deviceId 设备编号 + * @return 设备 Modbus 连接配置 + */ + IotDeviceModbusConfigDO getModbusConfigByDeviceId(Long deviceId); + + /** + * 获得设备 Modbus 连接配置分页 + * + * @param pageReqVO 分页查询 + * @return 设备 Modbus 连接配置分页 + */ + PageResult getModbusConfigPage(IotDeviceModbusConfigPageReqVO pageReqVO); + + /** + * 获得所有启用的 Modbus 连接配置列表 + * + * @return 启用的 Modbus 连接配置列表 + */ + List getEnabledModbusConfigList(); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java new file mode 100644 index 0000000..582258a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java @@ -0,0 +1,122 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusConfigMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODBUS_CONFIG_EXISTS; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODBUS_CONFIG_NOT_EXISTS; + +/** + * IoT 设备 Modbus 连接配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigService { + + @Resource + private IotDeviceModbusConfigMapper modbusConfigMapper; + + @Resource + private IotDeviceService deviceService; + + // TODO @AI:是不是搞成 save 接口?因为前端也不知道是 create 还是 update; + @Override + public Long createModbusConfig(IotDeviceModbusConfigSaveReqVO createReqVO) { + // 1.1 校验设备存在 + deviceService.validateDeviceExists(createReqVO.getDeviceId()); + // 1.2 校验设备是否已有 Modbus 配置 + validateModbusConfigUnique(createReqVO.getDeviceId(), null); + + // 2. 插入 + IotDeviceModbusConfigDO modbusConfig = BeanUtils.toBean(createReqVO, IotDeviceModbusConfigDO.class); + setDefaultValues(modbusConfig); + modbusConfigMapper.insert(modbusConfig); + return modbusConfig.getId(); + } + + @Override + public void updateModbusConfig(IotDeviceModbusConfigSaveReqVO updateReqVO) { + // 1.1 校验存在 + validateModbusConfigExists(updateReqVO.getId()); + // 1.2 校验设备存在 + deviceService.validateDeviceExists(updateReqVO.getDeviceId()); + // 1.3 校验唯一性 + validateModbusConfigUnique(updateReqVO.getDeviceId(), updateReqVO.getId()); + + // 2. 更新 + IotDeviceModbusConfigDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusConfigDO.class); + modbusConfigMapper.updateById(updateObj); + } + + @Override + public void deleteModbusConfig(Long id) { + // 校验存在 + validateModbusConfigExists(id); + // 删除 + modbusConfigMapper.deleteById(id); + } + + private void validateModbusConfigExists(Long id) { + if (modbusConfigMapper.selectById(id) == null) { + throw exception(DEVICE_MODBUS_CONFIG_NOT_EXISTS); + } + } + + private void validateModbusConfigUnique(Long deviceId, Long excludeId) { + IotDeviceModbusConfigDO config = modbusConfigMapper.selectByDeviceId(deviceId); + // TODO @AI:ObjUtil notequals + if (config != null && !config.getId().equals(excludeId)) { + throw exception(DEVICE_MODBUS_CONFIG_EXISTS); + } + } + + // TODO @AI:不要这个;前端都必须传递; + private void setDefaultValues(IotDeviceModbusConfigDO config) { + if (config.getPort() == null) { + config.setPort(502); + } + if (config.getSlaveId() == null) { + config.setSlaveId(1); + } + if (config.getTimeout() == null) { + config.setTimeout(3000); + } + if (config.getRetryInterval() == null) { + config.setRetryInterval(1000); + } + } + + @Override + public IotDeviceModbusConfigDO getModbusConfig(Long id) { + return modbusConfigMapper.selectById(id); + } + + @Override + public IotDeviceModbusConfigDO getModbusConfigByDeviceId(Long deviceId) { + return modbusConfigMapper.selectByDeviceId(deviceId); + } + + @Override + public PageResult getModbusConfigPage(IotDeviceModbusConfigPageReqVO pageReqVO) { + return modbusConfigMapper.selectPage(pageReqVO); + } + + @Override + public List getEnabledModbusConfigList() { + return modbusConfigMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java new file mode 100644 index 0000000..daf2b09 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * IoT 设备 Modbus 点位配置 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceModbusPointService { + + /** + * 创建设备 Modbus 点位配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createModbusPoint(@Valid IotDeviceModbusPointSaveReqVO createReqVO); + + /** + * 更新设备 Modbus 点位配置 + * + * @param updateReqVO 更新信息 + */ + void updateModbusPoint(@Valid IotDeviceModbusPointSaveReqVO updateReqVO); + + /** + * 删除设备 Modbus 点位配置 + * + * @param id 编号 + */ + void deleteModbusPoint(Long id); + + /** + * 获得设备 Modbus 点位配置 + * + * @param id 编号 + * @return 设备 Modbus 点位配置 + */ + IotDeviceModbusPointDO getModbusPoint(Long id); + + /** + * 获得设备 Modbus 点位配置分页 + * + * @param pageReqVO 分页查询 + * @return 设备 Modbus 点位配置分页 + */ + PageResult getModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO); + + /** + * 根据设备编号获得点位配置列表 + * + * @param deviceId 设备编号 + * @return 点位配置列表 + */ + List getModbusPointListByDeviceId(Long deviceId); + + /** + * 根据设备编号获得启用的点位配置列表 + * + * @param deviceId 设备编号 + * @return 启用的点位配置列表 + */ + List getEnabledModbusPointListByDeviceId(Long deviceId); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java new file mode 100644 index 0000000..0b79be8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java @@ -0,0 +1,138 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusPointMapper; +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +/** + * IoT 设备 Modbus 点位配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointService { + + @Resource + private IotDeviceModbusPointMapper modbusPointMapper; + + @Resource + private IotDeviceService deviceService; + + @Resource + private IotThingModelService thingModelService; + + @Override + public Long createModbusPoint(IotDeviceModbusPointSaveReqVO createReqVO) { + // 1.1 校验设备存在 + deviceService.validateDeviceExists(createReqVO.getDeviceId()); + // 1.2 校验物模型属性存在 + IotThingModelDO thingModel = validateThingModelExists(createReqVO.getThingModelId()); + // 1.3 校验同一设备下点位唯一性(基于 identifier) + validateModbusPointUnique(createReqVO.getDeviceId(), thingModel.getIdentifier(), null); + + // 2. 插入 + IotDeviceModbusPointDO modbusPoint = BeanUtils.toBean(createReqVO, IotDeviceModbusPointDO.class, + o -> o.setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName())); + modbusPointMapper.insert(modbusPoint); + return modbusPoint.getId(); + } + + @Override + public void updateModbusPoint(IotDeviceModbusPointSaveReqVO updateReqVO) { + // 1.1 校验存在 + validateModbusPointExists(updateReqVO.getId()); + // 1.2 校验设备存在 + deviceService.validateDeviceExists(updateReqVO.getDeviceId()); + // 1.3 校验物模型属性存在 + IotThingModelDO thingModel = validateThingModelExists(updateReqVO.getThingModelId()); + // 1.4 校验同一设备下点位唯一性 + validateModbusPointUnique(updateReqVO.getDeviceId(), thingModel.getIdentifier(), updateReqVO.getId()); + + // 2. 更新 + IotDeviceModbusPointDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusPointDO.class); + // TODO @AI:这块 + modbusPointMapper.updateById(updateObj); + } + + private IotThingModelDO validateThingModelExists(Long id) { + IotThingModelDO thingModel = thingModelService.getThingModel(id); + if (thingModel == null) { + throw exception(THING_MODEL_NOT_EXISTS); + } + return thingModel; + } + + @Override + public void deleteModbusPoint(Long id) { + // 校验存在 + validateModbusPointExists(id); + // 删除 + modbusPointMapper.deleteById(id); + } + + private IotDeviceModbusPointDO validateModbusPointExists(Long id) { + IotDeviceModbusPointDO point = modbusPointMapper.selectById(id); + if (point == null) { + throw exception(DEVICE_MODBUS_POINT_NOT_EXISTS); + } + return point; + } + + private void validateModbusPointUnique(Long deviceId, String identifier, Long excludeId) { + IotDeviceModbusPointDO point = modbusPointMapper.selectByDeviceIdAndIdentifier(deviceId, identifier); + // TODO @AI:ObjUtil notequals; + if (point != null && !point.getId().equals(excludeId)) { + throw exception(DEVICE_MODBUS_POINT_EXISTS); + } + } + + // TODO @AI:这块 + private void setDefaultValues(IotDeviceModbusPointDO point) { + if (point.getRegisterCount() == null) { + point.setRegisterCount(1); + } + if (point.getScale() == null) { + point.setScale(BigDecimal.ONE); + } + if (point.getPollInterval() == null) { + point.setPollInterval(5000); + } + } + + @Override + public IotDeviceModbusPointDO getModbusPoint(Long id) { + return modbusPointMapper.selectById(id); + } + + @Override + public PageResult getModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO) { + return modbusPointMapper.selectPage(pageReqVO); + } + + @Override + public List getModbusPointListByDeviceId(Long deviceId) { + return modbusPointMapper.selectListByDeviceId(deviceId); + } + + @Override + public List getEnabledModbusPointListByDeviceId(Long deviceId) { + return modbusPointMapper.selectListByDeviceIdAndStatus(deviceId, CommonStatusEnum.ENABLE.getStatus()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java index 29d540e..5dfbed0 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java @@ -4,6 +4,9 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; + +import java.util.List; /** * IoT 设备通用 API @@ -28,4 +31,11 @@ public interface IotDeviceCommonApi { */ CommonResult getDevice(IotDeviceGetReqDTO infoReqDTO); + /** + * 获取所有启用的 Modbus 设备配置列表 + * + * @return Modbus 设备配置列表 + */ + CommonResult> getEnabledModbusDeviceConfigs(); + } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java new file mode 100644 index 0000000..46d318a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.iot.core.biz.dto; + +import lombok.Data; + +import java.util.List; + +/** + * IoT Modbus 设备配置 Response DTO + * + * @author 芋道源码 + */ +@Data +public class IotModbusDeviceConfigRespDTO { + + /** + * 设备编号 + */ + private Long deviceId; + /** + * 产品标识 + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; + /** + * 租户编号 + */ + private Long tenantId; + + // ========== Modbus 连接配置 ========== + + /** + * Modbus 服务器 IP 地址 + */ + private String ip; + /** + * Modbus 端口 + */ + private Integer port; + /** + * 从站地址 + */ + private Integer slaveId; + /** + * 连接超时时间(毫秒) + */ + private Integer timeout; + /** + * 重试间隔(毫秒) + */ + private Integer retryInterval; + + // ========== 点位配置 ========== + + /** + * 点位列表 + */ + private List points; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java new file mode 100644 index 0000000..26596c5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.iot.core.biz.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * IoT Modbus 点位配置 Response DTO + * + * @author 芋道源码 + */ +@Data +public class IotModbusPointRespDTO { + + /** + * 点位编号 + */ + private Long id; + /** + * 属性标识符(物模型的 identifier) + */ + private String identifier; + /** + * 属性名称(物模型的 name) + */ + private String name; + + // ========== Modbus 协议配置 ========== + + // TODO @AI:所有的枚举,通过 @,不要写上去; + /** + * Modbus 功能码 + * + * 1-ReadCoils 2-ReadDiscreteInputs 3-ReadHoldingRegisters 4-ReadInputRegisters + */ + private Integer functionCode; + /** + * 寄存器起始地址 + */ + private Integer registerAddress; + /** + * 寄存器数量 + */ + private Integer registerCount; + /** + * 字节序 + * + * AB/BA/ABCD/CDAB/DCBA/BADC + */ + private String byteOrder; + /** + * 原始数据类型 + * + * INT16/UINT16/INT32/UINT32/FLOAT/DOUBLE/BOOLEAN/STRING + */ + private String rawDataType; + /** + * 缩放因子 + */ + private BigDecimal scale; + /** + * 轮询间隔(毫秒) + */ + private Integer pollInterval; + + // ========== 物模型相关字段 ========== + + // TODO @AI:分析一下,是否有必要返回 + /** + * 数据类型(来自物模型) + */ + private String dataType; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/pom.xml b/yudao-module-iot/yudao-module-iot-gateway/pom.xml index 8fde9dc..5d76c59 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/pom.xml +++ b/yudao-module-iot/yudao-module-iot-gateway/pom.xml @@ -33,7 +33,7 @@ org.apache.rocketmq rocketmq-spring-boot-starter - + true @@ -48,6 +48,13 @@ vertx-mqtt + + + com.ghgande + j2mod + 3.2.1 + + cn.iocoder.boot diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index 3e573ef..53aafda 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscr import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.*; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; @@ -194,4 +195,38 @@ public class IotGatewayConfiguration { } + /** + * IoT 网关 Modbus TCP 协议配置类 + */ + @Configuration + @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.modbus-tcp", name = "enabled", havingValue = "true") + @Slf4j + public static class ModbusTcpProtocolConfiguration { + + @Bean(name = "modbusTcpVertx", destroyMethod = "close") + public Vertx modbusTcpVertx() { + return Vertx.vertx(); + } + + @Bean + public IotModbusTcpUpstreamProtocol iotModbusTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, + IotDeviceMessageService messageService, + IotModbusTcpConnectionManager connectionManager, + IotModbusTcpPollScheduler pollScheduler, + IotModbusTcpConfigCacheService configCacheService, + IotModbusTcpUpstreamHandler upstreamHandler, + @Qualifier("modbusTcpVertx") Vertx modbusTcpVertx) { + return new IotModbusTcpUpstreamProtocol(gatewayProperties.getProtocol().getModbusTcp(), + messageService, connectionManager, pollScheduler, configCacheService, upstreamHandler, modbusTcpVertx); + } + + @Bean + public IotModbusTcpDownstreamSubscriber iotModbusTcpDownstreamSubscriber(IotModbusTcpUpstreamProtocol upstreamProtocol, + IotModbusTcpDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + return new IotModbusTcpDownstreamSubscriber(upstreamProtocol, downstreamHandler, messageBus); + } + + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 7655a37..5778003 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -93,6 +93,11 @@ public class IotGatewayProperties { */ private MqttWsProperties mqttWs; + /** + * Modbus TCP 组件配置 + */ + private ModbusTcpProperties modbusTcp; + } @Data @@ -503,4 +508,21 @@ public class IotGatewayProperties { } + @Data + public static class ModbusTcpProperties { + + /** + * 是否开启 + */ + @NotNull(message = "是否开启不能为空") + private Boolean enabled; + + /** + * 配置刷新间隔(秒) + */ + // TODO @AI:需要校验下非空; + private Integer configRefreshInterval = 30; + + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java new file mode 100644 index 0000000..ddec584 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusDataConverter.java @@ -0,0 +1,281 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// TODO @AI:注释上:希望 1 和 2,然后 3 只是辅助说明 +/** + * IoT Modbus 数据转换器 + * + * 负责: + * 1. 将 Modbus 原始寄存器值转换为物模型属性值 + * 2. 将物模型属性值转换为 Modbus 原始寄存器值 + * 3. 处理字节序、数据类型、缩放因子 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Component +@Slf4j +public class IotModbusDataConverter { + + /** + * 将原始值转换为物模型属性值 + * + * @param rawValues 原始值数组(寄存器值或线圈值) + * @param point 点位配置 + * @return 转换后的属性值 + */ + public Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) { + // TODO @AI:CollUtil.isEmpty; + if (rawValues == null || rawValues.length == 0) { + return null; + } + String rawDataType = point.getRawDataType(); + String byteOrder = point.getByteOrder(); + // TODO @AI:defaultIfNull; + BigDecimal scale = point.getScale() != null ? point.getScale() : BigDecimal.ONE; + + // 1. 根据原始数据类型解析原始数值 + Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder); + if (rawNumber == null) { + return null; + } + + // 2. 应用缩放因子:实际值 = 原始值 × scale + BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale); + + // 3. 根据数据类型返回合适的 Java 类型 + return formatValue(actualValue, rawDataType); + } + + /** + * 将物模型属性值转换为原始寄存器值 + * + * @param propertyValue 属性值 + * @param point 点位配置 + * @return 原始值数组 + */ + public int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) { + if (propertyValue == null) { + return new int[0]; + } + String rawDataType = point.getRawDataType(); + String byteOrder = point.getByteOrder(); + BigDecimal scale = point.getScale() != null ? point.getScale() : BigDecimal.ONE; + int registerCount = point.getRegisterCount() != null ? point.getRegisterCount() : 1; + + // 1. 转换为 BigDecimal + BigDecimal actualValue = new BigDecimal(propertyValue.toString()); + + // 2. 应用缩放因子:原始值 = 实际值 ÷ scale + BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP); + + // 3. 根据原始数据类型编码为寄存器值 + return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount); + } + + /** + * 解析原始值 + */ + private Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) { + // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的; + switch (rawDataType.toUpperCase()) { + case "BOOLEAN": + return rawValues[0] != 0 ? 1 : 0; + case "INT16": + return (short) rawValues[0]; + case "UINT16": + return rawValues[0] & 0xFFFF; + case "INT32": + return parseInt32(rawValues, byteOrder); + case "UINT32": + return parseUint32(rawValues, byteOrder); + case "FLOAT": + return parseFloat(rawValues, byteOrder); + case "DOUBLE": + return parseDouble(rawValues, byteOrder); + // TODO @AI:未知抛出异常; + default: + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); + return rawValues[0]; + } + } + + // TODO @AI:这些转换,有没一些工具类,可以优化;类似 hutool 的? + private int parseInt32(int[] rawValues, String byteOrder) { + if (rawValues.length < 2) { + return rawValues[0]; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt(); + } + + private long parseUint32(int[] rawValues, String byteOrder) { + if (rawValues.length < 2) { + return rawValues[0] & 0xFFFFFFFFL; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL; + } + + private float parseFloat(int[] rawValues, String byteOrder) { + if (rawValues.length < 2) { + return (float) rawValues[0]; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat(); + } + + private double parseDouble(int[] rawValues, String byteOrder) { + if (rawValues.length < 4) { + return rawValues[0]; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble(); + } + + /** + * 将寄存器值转换为字节数组 + */ + private byte[] registersToBytes(int[] registers, int count) { + byte[] bytes = new byte[count * 2]; + for (int i = 0; i < Math.min(registers.length, count); i++) { + bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF); + bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF); + } + return bytes; + } + + /** + * 根据字节序重排字节 + */ + private byte[] reorderBytes(byte[] bytes, String byteOrder) { + // 大端序,不需要调整 + // TODO @AI:StrUtil.equals;null 要抛出异常; + if (byteOrder == null || "ABCD".equals(byteOrder) || "AB".equals(byteOrder)) { + return bytes; + } + + // 其他字节序调整 + byte[] result = new byte[bytes.length]; + switch (byteOrder.toUpperCase()) { + // TODO @AI:走枚举;sortOrder; + case "BA": // 小端序(16 位) + if (bytes.length >= 2) { + result[0] = bytes[1]; + result[1] = bytes[0]; + } + break; + case "CDAB": // 大端字交换(32 位) + if (bytes.length >= 4) { + result[0] = bytes[2]; + result[1] = bytes[3]; + result[2] = bytes[0]; + result[3] = bytes[1]; + } + break; + case "DCBA": // 小端序(32 位) + if (bytes.length >= 4) { + result[0] = bytes[3]; + result[1] = bytes[2]; + result[2] = bytes[1]; + result[3] = bytes[0]; + } + break; + case "BADC": // 小端字交换(32 位) + if (bytes.length >= 4) { + result[0] = bytes[1]; + result[1] = bytes[0]; + result[2] = bytes[3]; + result[3] = bytes[2]; + } + break; + // TODO @AI:未知就抛出异常; + default: + return bytes; + } + return result; + } + + /** + * 编码为寄存器值 + */ + private int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) { + // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的; + switch (rawDataType.toUpperCase()) { + case "BOOLEAN": + return new int[]{rawValue.intValue() != 0 ? 1 : 0}; + case "INT16": + case "UINT16": + return new int[]{rawValue.intValue() & 0xFFFF}; + case "INT32": + case "UINT32": + return encodeInt32(rawValue.intValue(), byteOrder); + case "FLOAT": + return encodeFloat(rawValue.floatValue(), byteOrder); + case "DOUBLE": + return encodeDouble(rawValue.doubleValue(), byteOrder); + default: + return new int[]{rawValue.intValue()}; + } + } + + // TODO @AI:这些转换,有没一些工具类,可以优化;类似 hutool 的? + private int[] encodeInt32(int value, String byteOrder) { + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array(); + bytes = reorderBytes(bytes, byteOrder); + return bytesToRegisters(bytes); + } + + private int[] encodeFloat(float value, String byteOrder) { + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array(); + bytes = reorderBytes(bytes, byteOrder); + return bytesToRegisters(bytes); + } + + private int[] encodeDouble(double value, String byteOrder) { + byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array(); + bytes = reorderBytes(bytes, byteOrder); + return bytesToRegisters(bytes); + } + + private int[] bytesToRegisters(byte[] bytes) { + int[] registers = new int[bytes.length / 2]; + for (int i = 0; i < registers.length; i++) { + registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF); + } + return registers; + } + + /** + * 格式化返回值 + */ + private Object formatValue(BigDecimal value, String rawDataType) { + // TODO @AI:是不是可以用枚举?复用 IotModbusRawDataTypeEnum 里的; + switch (rawDataType.toUpperCase()) { + case "BOOLEAN": + return value.intValue() != 0; + case "INT16": + case "INT32": + return value.intValue(); + case "UINT16": + case "UINT32": + return value.longValue(); + case "FLOAT": + return value.floatValue(); + case "DOUBLE": + return value.doubleValue(); + // TODO @AI:未知抛出异常; + default: + return value; + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java new file mode 100644 index 0000000..43a2fbe --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpClient.java @@ -0,0 +1,195 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; +import com.ghgande.j2mod.modbus.msg.*; +import com.ghgande.j2mod.modbus.procimg.InputRegister; +import com.ghgande.j2mod.modbus.procimg.Register; +import com.ghgande.j2mod.modbus.procimg.SimpleRegister; +import com.ghgande.j2mod.modbus.util.BitVector; +import io.vertx.core.Future; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * IoT Modbus TCP 客户端 + * + * 负责: + * 1. 封装 Modbus 读写操作 + * 2. 根据功能码执行对应的 Modbus 请求 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Component +@RequiredArgsConstructor // TODO @AI:这个注解,是不是可以去掉? +@Slf4j +public class IotModbusTcpClient { + + /** + * 读取 Modbus 数据 + * + * @param connection Modbus 连接 + * @param slaveId 从站地址 + * @param point 点位配置 + * @return 原始值(int 数组) + */ + public Future read(IotModbusTcpConnectionManager.ModbusConnection connection, + Integer slaveId, + IotModbusPointRespDTO point) { + return connection.executeBlocking(tcpConnection -> { + try { + // 1. 创建请求 + ModbusRequest request = createReadRequest(point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount()); + request.setUnitID(slaveId); + + // 2. 执行事务(请求) + ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection); + transaction.setRequest(request); + transaction.execute(); + + // 3. 解析响应 + ModbusResponse response = transaction.getResponse(); + return extractValues(response, point.getFunctionCode()); + } catch (Exception e) { + // TODO @AI:抛出异常时,增加更多的上下文信息,比如设备、点位等 + throw new RuntimeException("Modbus 读取失败", e); + } + }); + } + + /** + * 写入 Modbus 数据 + * + * @param connection Modbus 连接 + * @param slaveId 从站地址 + * @param point 点位配置 + * @param values 要写入的值 + * @return 是否成功 + */ + public Future write(IotModbusTcpConnectionManager.ModbusConnection connection, + Integer slaveId, + IotModbusPointRespDTO point, + int[] values) { + return connection.executeBlocking(tcpConnection -> { + try { + // 1. 创建请求 + ModbusRequest request = createWriteRequest(point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount(), values); + if (request == null) { + throw new RuntimeException("功能码 " + point.getFunctionCode() + " 不支持写操作"); + } + request.setUnitID(slaveId); + + // 2. 执行事务(请求) + ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection); + transaction.setRequest(request); + transaction.execute(); + return true; + } catch (Exception e) { + // TODO @AI:抛出异常时,增加更多的上下文信息,比如设备、点位等; + throw new RuntimeException("Modbus 写入失败", e); + } + }); + } + + /** + * 创建读取请求 + */ + private ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { + // TODO @AI:1、2、3、4 能不能有枚举哈?这样 1、2、3、4 那的注释就不用写; + switch (functionCode) { + case 1: // ReadCoils + return new ReadCoilsRequest(address, count); + case 2: // ReadDiscreteInputs + return new ReadInputDiscretesRequest(address, count); + case 3: // ReadHoldingRegisters + return new ReadMultipleRegistersRequest(address, count); + case 4: // ReadInputRegisters + return new ReadInputRegistersRequest(address, count); + default: + throw new IllegalArgumentException("不支持的功能码: " + functionCode); + } + } + + /** + * 创建写入请求 + */ + private ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { + // TODO @AI:5、6、15、16 能不能有枚举哈?这样 5、6、15、16 那的注释就不用写; + switch (functionCode) { + case 1: // WriteCoils (使用 5 或 15) + if (count == 1) { + return new WriteCoilRequest(address, values[0] != 0); + } else { + // 多线圈写入 + BitVector bv = new BitVector(count); + for (int i = 0; i < Math.min(values.length, count); i++) { + bv.setBit(i, values[i] != 0); + } + return new WriteMultipleCoilsRequest(address, bv); + } + case 3: // WriteHoldingRegisters (使用 6 或 16) + if (count == 1) { + return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0])); + } else { + Register[] registers = new com.ghgande.j2mod.modbus.procimg.SimpleRegister[count]; + for (int i = 0; i < count; i++) { + registers[i] = new SimpleRegister(i < values.length ? values[i] : 0); + } + return new WriteMultipleRegistersRequest(address, registers); + } + case 2: // ReadDiscreteInputs - 只读 + case 4: // ReadInputRegisters - 只读 + return null; + default: + throw new IllegalArgumentException("不支持的功能码: " + functionCode); + } + } + + /** + * 从响应中提取值 + */ + private int[] extractValues(ModbusResponse response, Integer functionCode) { + // TODO @AI:1、2、3、4 能不能有枚举哈?这样 1、2、3、4 那的注释就不用写; + switch (functionCode) { + case 1: // ReadCoils + ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; + int bitCount = coilsResponse.getBitCount(); + int[] coilValues = new int[bitCount]; + for (int i = 0; i < bitCount; i++) { + coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0; + } + return coilValues; + case 2: // ReadDiscreteInputs + ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response; + int discreteCount = discretesResponse.getBitCount(); + int[] discreteValues = new int[discreteCount]; + for (int i = 0; i < discreteCount; i++) { + discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0; + } + return discreteValues; + case 3: // ReadHoldingRegisters + ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response; + InputRegister[] holdingRegisters = holdingResponse.getRegisters(); + int[] holdingValues = new int[holdingRegisters.length]; + for (int i = 0; i < holdingRegisters.length; i++) { + holdingValues[i] = holdingRegisters[i].getValue(); + } + return holdingValues; + case 4: // ReadInputRegisters + ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response; + InputRegister[] inputRegisters = inputResponse.getRegisters(); + int[] inputValues = new int[inputRegisters.length]; + for (int i = 0; i < inputRegisters.length; i++) { + inputValues[i] = inputRegisters[i].getValue(); + } + return inputValues; + default: + throw new IllegalArgumentException("不支持的功能码: " + functionCode); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java new file mode 100644 index 0000000..38744bf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfigCacheService.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +// TODO @AI:是不是 1、2、3 注释可以合并下; +/** + * IoT Modbus TCP 配置缓存服务 + * + * 负责: + * 1. 从 biz 拉取 Modbus 设备配置 + * 2. 缓存配置数据 + * 3. 检测配置变更 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Service +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpConfigCacheService { + + private final IotDeviceCommonApi deviceApi; + + /** + * 配置缓存:deviceId -> 配置 + */ + private final Map configCache = new ConcurrentHashMap<>(); + + /** + * 已知的设备 ID 集合 + */ + private final Set knownDeviceIds = ConcurrentHashMap.newKeySet(); + + /** + * 刷新配置 + * + * @return 最新的配置列表 + */ + public List refreshConfig() { + try { + // 1. 从远程获取配置 + CommonResult> result = deviceApi.getEnabledModbusDeviceConfigs(); + if (result == null || !result.isSuccess() || result.getData() == null) { + log.warn("[refreshConfig][获取 Modbus 配置失败: {}]", result); + return new ArrayList<>(configCache.values()); + } + List configs = result.getData(); + + // 2. 更新缓存 + for (IotModbusDeviceConfigRespDTO config : configs) { + configCache.put(config.getDeviceId(), config); + knownDeviceIds.add(config.getDeviceId()); + } + return configs; + } catch (Exception e) { + log.error("[refreshConfig][刷新配置失败]", e); + return new ArrayList<>(configCache.values()); + } + } + + /** + * 获取设备配置 + * + * @param deviceId 设备 ID + * @return 配置 + */ + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { + return configCache.get(deviceId); + } + + /** + * 清理已删除设备的资源 + * + * @param currentConfigs 当前有效的配置列表 + * @param cleanupAction 清理动作 + */ + public void cleanupRemovedDevices(List currentConfigs, Consumer cleanupAction) { + // 1.1 获取当前有效的设备 ID + // TODO @AI:convertSet 简化; + Set currentDeviceIds = new HashSet<>(); + for (IotModbusDeviceConfigRespDTO config : currentConfigs) { + currentDeviceIds.add(config.getDeviceId()); + } + // 1.2 找出已删除的设备 + Set removedDeviceIds = new HashSet<>(knownDeviceIds); + removedDeviceIds.removeAll(currentDeviceIds); + + // 2. 清理已删除设备 + for (Long deviceId : removedDeviceIds) { + log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); + configCache.remove(deviceId); + knownDeviceIds.remove(deviceId); + cleanupAction.accept(deviceId); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java new file mode 100644 index 0000000..75b98ea --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConnectionManager.java @@ -0,0 +1,255 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import com.ghgande.j2mod.modbus.net.TCPMasterConnection; +import io.vertx.core.Context; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Component; + +import java.net.InetAddress; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT Modbus TCP 连接管理器 + * + * 负责: + * 1. 管理 TCP 连接(相同 ip:port 共用连接) + * 2. 分布式锁管理(连接级别) + * 3. 连接重试和故障恢复 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Component +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpConnectionManager { + + // TODO @AI:iot:modbus-tcp:connection: + private static final String LOCK_KEY_PREFIX = "iot:modbus:connection:"; + + private final RedissonClient redissonClient; + private final Vertx vertx; + + /** + * 连接池:key = ip:port + */ + private final Map connectionPool = new ConcurrentHashMap<>(); + + /** + * 设备 ID 到连接 key 的映射 + */ + private final Map deviceConnectionMap = new ConcurrentHashMap<>(); + + /** + * 确保连接存在 + * + * @param config 设备配置 + */ + public void ensureConnection(IotModbusDeviceConfigRespDTO config) { + // 1. 记录设备与连接的关系 + String connectionKey = buildConnectionKey(config.getIp(), config.getPort()); + deviceConnectionMap.put(config.getDeviceId(), connectionKey); + + // 2. 情况一:连接已存在,添加设备引用 + ModbusConnection connection = connectionPool.get(connectionKey); + if (connection != null) { + // 添加设备引用 + connection.addDevice(config.getDeviceId(), config.getSlaveId()); + // 更新连接参数(取最小值) + // TODO @AI:(不确定)如果后续最小值被移除后,是不是无法灰度到上一个最小值? + connection.updateParams(config.getTimeout(), config.getRetryInterval()); + return; + } + + // 3. 情况二:连接不存在,创建新连接 + // 3.1 尝试获取分布式锁 + RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + connectionKey); + if (!lock.tryLock()) { + log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey); + return; + } + // 3.2 创建新连接 + try { + connection = createConnection(config, lock); + connectionPool.put(connectionKey, connection); + log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey); + } catch (Exception e) { + log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e); + lock.unlock(); + } + } + + /** + * 创建 Modbus TCP 连接 + */ + private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config, RLock lock) throws Exception { + // 创建 TCP 连接 + // TODO @AI:需要重连么? + TCPMasterConnection tcpConnection = new TCPMasterConnection(InetAddress.getByName(config.getIp())); + tcpConnection.setPort(config.getPort()); + tcpConnection.setTimeout(config.getTimeout()); + tcpConnection.connect(); + + // 创建 Modbus 连接对象 + ModbusConnection connection = new ModbusConnection(); + // TODO @AI:链式调用,简化下; + connection.setConnectionKey(buildConnectionKey(config.getIp(), config.getPort())); + connection.setTcpConnection(tcpConnection); + connection.setLock(lock); + connection.setTimeout(config.getTimeout()); + connection.setRetryInterval(config.getRetryInterval()); + connection.setContext(vertx.getOrCreateContext()); + connection.addDevice(config.getDeviceId(), config.getSlaveId()); + return connection; + } + + /** + * 获取连接 + */ + public ModbusConnection getConnection(Long deviceId) { + String connectionKey = deviceConnectionMap.get(deviceId); + if (connectionKey == null) { + return null; + } + return connectionPool.get(connectionKey); + } + + /** + * 获取设备的 slave ID + */ + public Integer getSlaveId(Long deviceId) { + ModbusConnection connection = getConnection(deviceId); + if (connection == null) { + return null; + } + return connection.getSlaveId(deviceId); + } + + /** + * 移除设备 + */ + public void removeDevice(Long deviceId) { + // 1. 移除设备引用 + String connectionKey = deviceConnectionMap.remove(deviceId); + if (connectionKey == null) { + return; + } + ModbusConnection connection = connectionPool.get(connectionKey); + if (connection == null) { + return; + } + connection.removeDevice(deviceId); + + // 2. 如果没有设备引用了,关闭连接 + if (connection.getDeviceCount() == 0) { + closeConnection(connectionKey); + } + } + + /** + * 关闭指定连接 + */ + private void closeConnection(String connectionKey) { + ModbusConnection connection = connectionPool.remove(connectionKey); + if (connection == null) { + return; + } + + try { + if (connection.getTcpConnection() != null) { + connection.getTcpConnection().close(); + } + // TODO @AI:(不确定)是不是要当前线程?还是当前进程就 ok 了。 + if (connection.getLock() != null && connection.getLock().isHeldByCurrentThread()) { + connection.getLock().unlock(); + } + log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey); + } catch (Exception e) { + log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e); + } + } + + /** + * 关闭所有连接 + */ + public void closeAll() { + for (String connectionKey : connectionPool.keySet()) { + closeConnection(connectionKey); + } + deviceConnectionMap.clear(); + } + + private String buildConnectionKey(String ip, Integer port) { + return ip + ":" + port; + } + + /** + * Modbus 连接信息 + */ + @Data + public static class ModbusConnection { + + private String connectionKey; + private TCPMasterConnection tcpConnection; + private RLock lock; + private Integer timeout; + private Integer retryInterval; + private Context context; + + /** + * 设备 ID 到 slave ID 的映射 + */ + private final Map deviceSlaveMap = new ConcurrentHashMap<>(); + + public void addDevice(Long deviceId, Integer slaveId) { + deviceSlaveMap.put(deviceId, slaveId); + } + + public void removeDevice(Long deviceId) { + deviceSlaveMap.remove(deviceId); + } + + public int getDeviceCount() { + return deviceSlaveMap.size(); + } + + public Integer getSlaveId(Long deviceId) { + return deviceSlaveMap.get(deviceId); + } + + public void updateParams(Integer timeout, Integer retryInterval) { + // 取最小值 + if (timeout != null && (this.timeout == null || timeout < this.timeout)) { + this.timeout = timeout; + } + if (retryInterval != null && (this.retryInterval == null || retryInterval < this.retryInterval)) { + this.retryInterval = retryInterval; + } + } + + /** + * 执行 Modbus 读取操作(阻塞方式,在 Vert.x worker 线程执行) + */ + public Future executeBlocking(java.util.function.Function operation) { + // TODO @AI:executeBlocking 方法,已经废弃了,看看要不要替换。 + // TODO @AI:疑问。这里会不会线程阻塞?多个设备之间,担心性能; + return context.executeBlocking(promise -> { + try { + T result = operation.apply(tcpConnection); + promise.complete(result); + } catch (Exception e) { + promise.fail(e); + } + }, true); // ordered=true 保证同一连接串行执行 + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java new file mode 100644 index 0000000..eef237f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamHandler.java @@ -0,0 +1,128 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; + +// TODO @AI:看看注释能不能优化下; +/** + * IoT Modbus TCP 下行消息处理器 + * + * 负责: + * 1. 处理属性设置消息(thing.service.property.set) + * 2. 将属性值转换为 Modbus 原始值 + * 3. 执行 Modbus 写入操作 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Component +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpDownstreamHandler { + + private final IotModbusTcpConnectionManager connectionManager; + private final IotModbusTcpClient modbusClient; + private final IotModbusDataConverter dataConverter; + private final IotModbusTcpConfigCacheService configCacheService; + + /** + * 处理下行消息 + */ + @SuppressWarnings("unchecked") + public void handle(IotDeviceMessage message) { + // 1.1 检查是否是属性设置消息 + if (!"thing.service.property.set".equals(message.getMethod())) { + log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); + return; + } + // 1.2 获取设备配置 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId()); + if (config == null) { + log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId()); + return; + } + + // 2. 解析属性值并写入 + Object params = message.getParams(); + if (!(params instanceof Map)) { + log.warn("[handle][params 不是 Map 类型: {}]", params); + return; + } + Map propertyMap = (Map) params; + for (Map.Entry entry : propertyMap.entrySet()) { + String identifier = entry.getKey(); + Object value = entry.getValue(); + // 2.1.1 查找对应的点位配置 + IotModbusPointRespDTO point = findPoint(config, identifier); + if (point == null) { + log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); + continue; + } + // 2.1.2 检查是否支持写操作 + if (!isWritable(point.getFunctionCode())) { + log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); + continue; + } + + // 2.2 执行写入 + writeProperty(config, point, value); + } + } + + /** + * 写入属性值 + */ + private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) { + // 1.1 获取连接 + IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); + if (connection == null) { + log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId()); + return; + } + // 1.2 获取 slave ID + Integer slaveId = connectionManager.getSlaveId(config.getDeviceId()); + if (slaveId == null) { + log.warn("[writeProperty][设备 {} 没有 slaveId]", config.getDeviceId()); + return; + } + + // 2.1 转换属性值为原始值 + int[] rawValues = dataConverter.convertToRawValues(value, point); + // 2.2 执行 Modbus 写入 + modbusClient.write(connection, slaveId, point, rawValues) + .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", + config.getDeviceId(), point.getIdentifier(), value)) + .onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]", + config.getDeviceId(), point.getIdentifier(), e)); + } + + /** + * 查找点位配置 + */ + private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { + if (config.getPoints() == null) { + return null; + } + // TODO @AI:hutool findOne? + return config.getPoints().stream() + .filter(p -> identifier.equals(p.getIdentifier())) + .findFirst() + .orElse(null); + } + + /** + * 检查功能码是否支持写操作 + */ + private boolean isWritable(Integer functionCode) { + // TODO @AI:能不能通过 枚举优化下? + // 功能码 1(ReadCoils)和 3(ReadHoldingRegisters)支持写操作 + return functionCode != null && (functionCode == 1 || functionCode == 3); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java new file mode 100644 index 0000000..241faa8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpDownstreamSubscriber.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT Modbus TCP 下行消息订阅器 + * + * 负责: + * 1. 订阅消息总线的下行消息 + * 2. 将属性设置消息转发给下行处理器 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber { + + private final IotModbusTcpUpstreamProtocol upstreamProtocol; + private final IotModbusTcpDownstreamHandler downstreamHandler; + private final IotMessageBus messageBus; + + @PostConstruct + public void subscribe() { + messageBus.register(this); + log.info("[subscribe][Modbus TCP 下行消息订阅器已启动, topic={}]", getTopic()); + } + + @Override + public String getTopic() { + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId()); + } + + @Override + public String getGroup() { + return getTopic(); // 点对点消费 + } + + @Override + public void onMessage(IotDeviceMessage message) { + log.debug("[onMessage][收到下行消息: {}]", message); + try { + downstreamHandler.handle(message); + } catch (Exception e) { + log.error("[onMessage][处理下行消息失败]", e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java new file mode 100644 index 0000000..e4b3192 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java @@ -0,0 +1,134 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import io.vertx.core.Vertx; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT Modbus TCP 轮询调度器 + * + * 负责: + * 1. 管理每个点位的轮询定时器 + * 2. 调度 Modbus 读取任务 + * 3. 处理读取结果并上报 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Component +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpPollScheduler { + + private final Vertx vertx; + private final IotModbusTcpConnectionManager connectionManager; + private final IotModbusTcpClient modbusClient; + private final IotModbusTcpUpstreamHandler upstreamHandler; + + /** + * 设备的定时器列表:deviceId -> timerId 列表 + */ + private final Map> deviceTimers = new ConcurrentHashMap<>(); + + /** + * 更新轮询任务 + */ + public void updatePolling(IotModbusDeviceConfigRespDTO config) { + Long deviceId = config.getDeviceId(); + + // 1. 停止旧的轮询任务 + stopPolling(deviceId); + + // 2.1 为每个点位创建新的轮询任务 + List timerIds = new ArrayList<>(); + // TODO @AI:if return 简化;上面的 size 加下;config.getPoints() + if (config.getPoints() != null) { + for (IotModbusPointRespDTO point : config.getPoints()) { + Long timerId = createPollTimer(config, point); + if (timerId != null) { + timerIds.add(timerId); + } + } + } + // 2.2 记录定时器 + // TODO @AI:CollUtil.isNotEmpty;if return 简化; + if (!timerIds.isEmpty()) { + deviceTimers.put(deviceId, timerIds); + log.debug("[updatePolling][设备 {} 创建了 {} 个轮询定时器]", deviceId, timerIds.size()); + } + } + + /** + * 创建轮询定时器 + */ + private Long createPollTimer(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { + if (point.getPollInterval() == null || point.getPollInterval() <= 0) { + return null; + } + return vertx.setPeriodic(point.getPollInterval(), timerId -> { + try { + pollPoint(config, point); + } catch (Exception e) { + log.error("[createPollTimer][轮询点位失败, deviceId={}, identifier={}]", + config.getDeviceId(), point.getIdentifier(), e); + } + }); + } + + /** + * 轮询单个点位 + */ + private void pollPoint(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { + // 1.1 获取连接 + IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); + if (connection == null) { + log.warn("[pollPoint][设备 {} 没有连接]", config.getDeviceId()); + return; + } + // 1.2 获取 slave ID + Integer slaveId = connectionManager.getSlaveId(config.getDeviceId()); + if (slaveId == null) { + log.warn("[pollPoint][设备 {} 没有 slaveId]", config.getDeviceId()); + return; + } + + // 2. 执行 Modbus 读取 + // TODO @AI:超时时间,有实现么??? + modbusClient.read(connection, slaveId, point) + .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue)) + .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]", + config.getDeviceId(), point.getIdentifier(), e)); + } + + /** + * 停止设备的轮询 + */ + public void stopPolling(Long deviceId) { + List timerIds = deviceTimers.remove(deviceId); + // TODO @AI:CollUtil.isNotEmpty;并且 if return; + if (timerIds != null) { + for (Long timerId : timerIds) { + vertx.cancelTimer(timerId); + } + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timerIds.size()); + } + } + + /** + * 停止所有轮询 + */ + public void stopAll() { + for (Long deviceId : deviceTimers.keySet()) { + stopPolling(deviceId); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java new file mode 100644 index 0000000..df46228 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamHandler.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * IoT Modbus TCP 上行数据处理器 + * + * 负责: + * 1. 将 Modbus 读取的原始值转换为物模型属性值 + * 2. 构造属性上报消息 + * 3. 发送消息到消息总线 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@Component +@Slf4j +public class IotModbusTcpUpstreamHandler { + + private final IotDeviceMessageService messageService; + private final IotModbusDataConverter dataConverter; + + private String serverId; + + public IotModbusTcpUpstreamHandler(IotDeviceMessageService messageService, + IotModbusDataConverter dataConverter) { + this.messageService = messageService; + this.dataConverter = dataConverter; + } + + // TODO @AI:lombok 简化 + /** + * 设置 serverId + * + * @param serverId 服务器 ID + */ + public void setServerId(String serverId) { + this.serverId = serverId; + } + + /** + * 处理 Modbus 读取结果 + * + * @param config 设备配置 + * @param point 点位配置 + * @param rawValue 原始值(int 数组) + */ + public void handleReadResult(IotModbusDeviceConfigRespDTO config, + IotModbusPointRespDTO point, + int[] rawValue) { + try { + // 1.1 转换原始值为物模型属性值 + Object convertedValue = dataConverter.convertToPropertyValue(rawValue, point); + log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]", + config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue); + // 1.2 构造属性上报消息 + Map params = new HashMap<>(); + params.put(point.getIdentifier(), convertedValue); + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.property.post", params); + + // 2. 发送到消息总线 + messageService.sendDeviceMessage(message, config.getProductKey(), + config.getDeviceName(), serverId); + } catch (Exception e) { + log.error("[handleReadResult][处理读取结果失败, deviceId={}, identifier={}]", + config.getDeviceId(), point.getIdentifier(), e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java new file mode 100644 index 0000000..1717e61 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java @@ -0,0 +1,128 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * IoT Modbus TCP 上行协议 + * + * 负责: + * 1. 定时从 biz 拉取 Modbus 设备配置 + * 2. 管理 Modbus TCP 连接 + * 3. 调度轮询任务 + * 4. 处理采集数据上报 + * + * @author 芋道源码 + */ +// TODO @AI:希望它的初始化,在 configuration 里; +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpUpstreamProtocol { + + private final IotGatewayProperties.ModbusTcpProperties modbusTcpProperties; + private final IotDeviceMessageService messageService; + private final IotModbusTcpConnectionManager connectionManager; + private final IotModbusTcpPollScheduler pollScheduler; + private final IotModbusTcpConfigCacheService configCacheService; + private final IotModbusTcpUpstreamHandler upstreamHandler; + private final Vertx vertx; + + // TODO @AI:按照别的模块,生成 serverId; + /** + * 服务器 ID,用于标识当前网关实例 + */ + private final String serverId = UUID.randomUUID().toString(); + + /** + * 配置刷新定时器 ID + */ + private Long configRefreshTimerId; + + @PostConstruct + public void start() { + log.info("[start][Modbus TCP 协议启动, serverId={}]", serverId); + + // 0. 设置 serverId 到上行处理器 + upstreamHandler.setServerId(serverId); + + // 1. 首次加载配置 + refreshConfig(); + + // 2. 启动配置定时刷新 + int refreshInterval = modbusTcpProperties.getConfigRefreshInterval(); + configRefreshTimerId = vertx.setPeriodic( + TimeUnit.SECONDS.toMillis(refreshInterval), + id -> refreshConfig() + ); + log.info("[start][配置刷新定时器已启动, 间隔={}秒]", refreshInterval); + } + + @PreDestroy + public void stop() { + log.info("[stop][Modbus TCP 协议停止]"); + + // 1. 取消配置刷新定时器 + if (configRefreshTimerId != null) { + vertx.cancelTimer(configRefreshTimerId); + } + + // 2. 停止轮询调度器 + pollScheduler.stopAll(); + + // 3. 关闭所有连接 + connectionManager.closeAll(); + } + + /** + * 刷新配置 + */ + private void refreshConfig() { + try { + // 1. 从 biz 拉取最新配置 + List configs = configCacheService.refreshConfig(); + log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); + + // 2. 更新连接和轮询任务 + for (IotModbusDeviceConfigRespDTO config : configs) { + try { + // 2.1 确保连接存在 + connectionManager.ensureConnection(config); + // 2.2 更新轮询任务 + // TODO @AI:【重要】如果点位配置没变化,是不是不用 update? + pollScheduler.updatePolling(config); + } catch (Exception e) { + log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); + } + } + + // 3. 清理已删除设备的资源 + configCacheService.cleanupRemovedDevices(configs, deviceId -> { + pollScheduler.stopPolling(deviceId); + connectionManager.removeDevice(deviceId); + }); + } catch (Exception e) { + log.error("[refreshConfig][刷新配置失败]", e); + } + } + + // TODO @AI:是不是 lombok 简化 + public String getServerId() { + return serverId; + } + + // TODO @AI:不需要;可以删除; + public IotDeviceMessageService getMessageService() { + return messageService; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java index b325103..86940b2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; @@ -18,6 +19,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import java.util.List; + import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; /** @@ -33,6 +36,7 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { private IotGatewayProperties gatewayProperties; private RestTemplate restTemplate; + private RestTemplate modbusRestTemplate; @PostConstruct public void init() { @@ -42,24 +46,35 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { .readTimeout(rpc.getReadTimeout()) .connectTimeout(rpc.getConnectTimeout()) .build(); + // TODO @AI:继续复用 rpc.getConnectTimeout();不需要独立 modbusRestTemplate + modbusRestTemplate = new RestTemplateBuilder() + .rootUri(rpc.getUrl() + "/rpc-api/iot/modbus") + .readTimeout(rpc.getReadTimeout()) + .connectTimeout(rpc.getConnectTimeout()) + .build(); } @Override public CommonResult authDevice(IotDeviceAuthReqDTO authReqDTO) { - return doPost("/auth", authReqDTO, new ParameterizedTypeReference<>() { }); + return doPost(restTemplate, "/auth", authReqDTO, new ParameterizedTypeReference<>() { }); } @Override public CommonResult getDevice(IotDeviceGetReqDTO getReqDTO) { - return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { }); + return doPost(restTemplate, "/get", getReqDTO, new ParameterizedTypeReference<>() { }); + } + + @Override + public CommonResult> getEnabledModbusDeviceConfigs() { + return doPost(modbusRestTemplate, "/enabled-configs", null, new ParameterizedTypeReference<>() { }); } - private CommonResult doPost(String url, T body, + private CommonResult doPost(RestTemplate template, String url, T body, ParameterizedTypeReference> responseType) { try { // 请求 HttpEntity requestEntity = new HttpEntity<>(body); - ResponseEntity> response = restTemplate.exchange( + ResponseEntity> response = template.exchange( url, HttpMethod.POST, requestEntity, responseType); // 响应 CommonResult result = response.getBody(); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index f633f1c..8ccdcb5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -117,6 +117,9 @@ yudao: keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒) ssl-enabled: false # 是否启用 SSL(wss://) sub-protocol: mqtt # WebSocket 子协议 + # TODO @AI:modbus-tcp 组件配置待补充 + modbus-tcp: + enabled: true --- #################### 日志相关配置 ####################