Commit 9e2c1feae10b353cbb8bcb49a7bb5b643fe2c212

Authored by YunaiV
1 parent 5086e122

feat:【iot】modbus-tcp 协议接入:10% 初始化:基于 cheeky-plotting-pumpkin.md 规划

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