mirror of
https://gitee.com/zhijiantianya/ruoyi-vue-pro.git
synced 2026-03-22 05:07:17 +08:00
!1498 feat:【iot】modbus-tcp 协议接入:100%
Merge pull request !1498 from 芋道源码/feature/iot-modbus
This commit is contained in:
@@ -1,31 +1,41 @@
|
||||
package cn.iocoder.yudao.module.iot.api.device;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.RpcConstants;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.*;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService;
|
||||
import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService;
|
||||
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
|
||||
|
||||
/**
|
||||
* IoT 设备 API 实现类
|
||||
@@ -41,6 +51,12 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
|
||||
private IotDeviceService deviceService;
|
||||
@Resource
|
||||
private IotProductService productService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,解决循环依赖
|
||||
private IotDeviceModbusConfigService modbusConfigService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,解决循环依赖
|
||||
private IotDeviceModbusPointService modbusPointService;
|
||||
|
||||
@Override
|
||||
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth")
|
||||
@@ -63,6 +79,52 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/config-list")
|
||||
@PermitAll
|
||||
@TenantIgnore
|
||||
public CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList(
|
||||
@RequestBody IotModbusDeviceConfigListReqDTO listReqDTO) {
|
||||
// 1. 获取 Modbus 连接配置
|
||||
List<IotDeviceModbusConfigDO> configList = modbusConfigService.getDeviceModbusConfigList(listReqDTO);
|
||||
if (CollUtil.isEmpty(configList)) {
|
||||
return success(new ArrayList<>());
|
||||
}
|
||||
|
||||
// 2. 组装返回结果
|
||||
Set<Long> deviceIds = convertSet(configList, IotDeviceModbusConfigDO::getDeviceId);
|
||||
Map<Long, IotDeviceDO> deviceMap = deviceService.getDeviceMap(deviceIds);
|
||||
Map<Long, List<IotDeviceModbusPointDO>> pointMap = modbusPointService.getEnabledDeviceModbusPointMapByDeviceIds(deviceIds);
|
||||
Map<Long, IotProductDO> productMap = productService.getProductMap(convertSet(deviceMap.values(), IotDeviceDO::getProductId));
|
||||
List<IotModbusDeviceConfigRespDTO> result = new ArrayList<>(configList.size());
|
||||
for (IotDeviceModbusConfigDO config : configList) {
|
||||
// 3.1 获取设备信息
|
||||
IotDeviceDO device = deviceMap.get(config.getDeviceId());
|
||||
if (device == null) {
|
||||
continue;
|
||||
}
|
||||
// 3.2 按 protocolType 筛选(如果非空)
|
||||
if (StrUtil.isNotEmpty(listReqDTO.getProtocolType())) {
|
||||
IotProductDO product = productMap.get(device.getProductId());
|
||||
if (product == null || ObjUtil.notEqual(listReqDTO.getProtocolType(), product.getProtocolType())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// 3.3 获取启用的点位列表
|
||||
List<IotDeviceModbusPointDO> pointList = pointMap.get(config.getDeviceId());
|
||||
if (CollUtil.isEmpty(pointList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3.4 构建 IotModbusDeviceConfigRespDTO 对象
|
||||
IotModbusDeviceConfigRespDTO configDTO = BeanUtils.toBean(config, IotModbusDeviceConfigRespDTO.class, o ->
|
||||
o.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())
|
||||
.setPoints(BeanUtils.toBean(pointList, IotModbusPointRespDTO.class)));
|
||||
result.add(configDTO);
|
||||
}
|
||||
return success(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register")
|
||||
@PermitAll
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigRespVO;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
|
||||
import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - IoT 设备 Modbus 连接配置")
|
||||
@RestController
|
||||
@RequestMapping("/iot/device-modbus-config")
|
||||
@Validated
|
||||
public class IotDeviceModbusConfigController {
|
||||
|
||||
@Resource
|
||||
private IotDeviceModbusConfigService modbusConfigService;
|
||||
|
||||
@PostMapping("/save")
|
||||
@Operation(summary = "保存设备 Modbus 连接配置")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:update')")
|
||||
public CommonResult<Boolean> saveDeviceModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO saveReqVO) {
|
||||
modbusConfigService.saveDeviceModbusConfig(saveReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得设备 Modbus 连接配置")
|
||||
@Parameter(name = "id", description = "编号", example = "1024")
|
||||
@Parameter(name = "deviceId", description = "设备编号", example = "2048")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:query')")
|
||||
public CommonResult<IotDeviceModbusConfigRespVO> getDeviceModbusConfig(
|
||||
@RequestParam(value = "id", required = false) Long id,
|
||||
@RequestParam(value = "deviceId", required = false) Long deviceId) {
|
||||
IotDeviceModbusConfigDO modbusConfig = null;
|
||||
if (id != null) {
|
||||
modbusConfig = modbusConfigService.getDeviceModbusConfig(id);
|
||||
} else if (deviceId != null) {
|
||||
modbusConfig = modbusConfigService.getDeviceModbusConfigByDeviceId(deviceId);
|
||||
}
|
||||
return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointRespVO;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO;
|
||||
import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - IoT 设备 Modbus 点位配置")
|
||||
@RestController
|
||||
@RequestMapping("/iot/device-modbus-point")
|
||||
@Validated
|
||||
public class IotDeviceModbusPointController {
|
||||
|
||||
@Resource
|
||||
private IotDeviceModbusPointService modbusPointService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建设备 Modbus 点位配置")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:update')")
|
||||
public CommonResult<Long> createDeviceModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO createReqVO) {
|
||||
return success(modbusPointService.createDeviceModbusPoint(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新设备 Modbus 点位配置")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:update')")
|
||||
public CommonResult<Boolean> updateDeviceModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO updateReqVO) {
|
||||
modbusPointService.updateDeviceModbusPoint(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除设备 Modbus 点位配置")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:update')")
|
||||
public CommonResult<Boolean> deleteDeviceModbusPoint(@RequestParam("id") Long id) {
|
||||
modbusPointService.deleteDeviceModbusPoint(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得设备 Modbus 点位配置")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:query')")
|
||||
public CommonResult<IotDeviceModbusPointRespVO> getDeviceModbusPoint(@RequestParam("id") Long id) {
|
||||
IotDeviceModbusPointDO modbusPoint = modbusPointService.getDeviceModbusPoint(id);
|
||||
return success(BeanUtils.toBean(modbusPoint, IotDeviceModbusPointRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得设备 Modbus 点位配置分页")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:query')")
|
||||
public CommonResult<PageResult<IotDeviceModbusPointRespVO>> getDeviceModbusPointPage(@Valid IotDeviceModbusPointPageReqVO pageReqVO) {
|
||||
PageResult<IotDeviceModbusPointDO> pageResult = modbusPointService.getDeviceModbusPointPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, IotDeviceModbusPointRespVO.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -38,7 +38,7 @@ public class IotDeviceMessageRespVO {
|
||||
@Schema(description = "请求编号", example = "req_123")
|
||||
private String requestId;
|
||||
|
||||
@Schema(description = "请求方法", requiredMode = Schema.RequiredMode.REQUIRED, example = "thing.property.report")
|
||||
@Schema(description = "请求方法", requiredMode = Schema.RequiredMode.REQUIRED, example = "thing.property.post")
|
||||
private String method;
|
||||
|
||||
@Schema(description = "请求参数")
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置 Response VO")
|
||||
@Data
|
||||
public class IotDeviceModbusConfigRespVO {
|
||||
|
||||
@Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "设备名称", example = "温湿度传感器")
|
||||
private String deviceName;
|
||||
|
||||
@Schema(description = "Modbus 服务器 IP 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100")
|
||||
private String ip;
|
||||
|
||||
@Schema(description = "Modbus 端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "502")
|
||||
private Integer port;
|
||||
|
||||
@Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer slaveId;
|
||||
|
||||
@Schema(description = "连接超时时间(毫秒)", example = "3000")
|
||||
private Integer timeout;
|
||||
|
||||
@Schema(description = "重试间隔(毫秒)", example = "1000")
|
||||
private Integer retryInterval;
|
||||
|
||||
@Schema(description = "工作模式", example = "1")
|
||||
private Integer mode;
|
||||
|
||||
@Schema(description = "数据帧格式", example = "1")
|
||||
private Integer frameFormat;
|
||||
|
||||
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置新增/修改 Request VO")
|
||||
@Data
|
||||
public class IotDeviceModbusConfigSaveReqVO {
|
||||
|
||||
@Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "设备编号不能为空")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100")
|
||||
private String ip;
|
||||
|
||||
@Schema(description = "Modbus 端口", example = "502")
|
||||
private Integer port;
|
||||
|
||||
@Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "从站地址不能为空")
|
||||
private Integer slaveId;
|
||||
|
||||
@Schema(description = "连接超时时间(毫秒)", example = "3000")
|
||||
private Integer timeout;
|
||||
|
||||
@Schema(description = "重试间隔(毫秒)", example = "1000")
|
||||
private Integer retryInterval;
|
||||
|
||||
@Schema(description = "工作模式", example = "1")
|
||||
@InEnum(IotModbusModeEnum.class)
|
||||
private Integer mode;
|
||||
|
||||
@Schema(description = "数据帧格式", example = "1")
|
||||
@InEnum(IotModbusFrameFormatEnum.class)
|
||||
private Integer frameFormat;
|
||||
|
||||
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "状态不能为空")
|
||||
private Integer status;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class IotDeviceModbusPointPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "设备编号", example = "1024")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "属性标识符", example = "temperature")
|
||||
private String identifier;
|
||||
|
||||
@Schema(description = "属性名称", example = "温度")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "Modbus 功能码", example = "3")
|
||||
private Integer functionCode;
|
||||
|
||||
@Schema(description = "状态", example = "0")
|
||||
private Integer status;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置 Response VO")
|
||||
@Data
|
||||
public class IotDeviceModbusPointRespVO {
|
||||
|
||||
@Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "物模型属性编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||
private Long thingModelId;
|
||||
|
||||
@Schema(description = "属性标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature")
|
||||
private String identifier;
|
||||
|
||||
@Schema(description = "属性名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "温度")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "Modbus 功能码", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
|
||||
private Integer functionCode;
|
||||
|
||||
@Schema(description = "寄存器起始地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
private Integer registerAddress;
|
||||
|
||||
@Schema(description = "寄存器数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer registerCount;
|
||||
|
||||
@Schema(description = "字节序", requiredMode = Schema.RequiredMode.REQUIRED, example = "AB")
|
||||
private String byteOrder;
|
||||
|
||||
@Schema(description = "原始数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "INT16")
|
||||
private String rawDataType;
|
||||
|
||||
@Schema(description = "缩放因子", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
|
||||
private BigDecimal scale;
|
||||
|
||||
@Schema(description = "轮询间隔(毫秒)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5000")
|
||||
private Integer pollInterval;
|
||||
|
||||
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置新增/修改 Request VO")
|
||||
@Data
|
||||
public class IotDeviceModbusPointSaveReqVO {
|
||||
|
||||
@Schema(description = "主键", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "设备编号不能为空")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "物模型属性编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||
@NotNull(message = "物模型属性编号不能为空")
|
||||
private Long thingModelId;
|
||||
|
||||
@Schema(description = "Modbus 功能码", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
|
||||
@NotNull(message = "Modbus 功能码不能为空")
|
||||
private Integer functionCode;
|
||||
|
||||
@Schema(description = "寄存器起始地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "寄存器起始地址不能为空")
|
||||
private Integer registerAddress;
|
||||
|
||||
@Schema(description = "寄存器数量", example = "1")
|
||||
private Integer registerCount;
|
||||
|
||||
@Schema(description = "字节序", requiredMode = Schema.RequiredMode.REQUIRED, example = "AB")
|
||||
@NotEmpty(message = "字节序不能为空")
|
||||
private String byteOrder;
|
||||
|
||||
@Schema(description = "原始数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "INT16")
|
||||
@NotEmpty(message = "原始数据类型不能为空")
|
||||
private String rawDataType;
|
||||
|
||||
@Schema(description = "缩放因子", example = "1.0")
|
||||
private BigDecimal scale;
|
||||
|
||||
@Schema(description = "轮询间隔(毫秒)", example = "5000")
|
||||
private Integer pollInterval;
|
||||
|
||||
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "状态不能为空")
|
||||
private Integer status;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
### 请求 /iot/product/sync-property-table 接口 => 成功
|
||||
POST {{baseUrl}}/iot/product/sync-property-table
|
||||
Content-Type: application/json
|
||||
tenant-id: {{adminTenantId}}
|
||||
Authorization: Bearer {{token}}
|
||||
@@ -141,6 +141,14 @@ public class IotProductController {
|
||||
result.getData().getList());
|
||||
}
|
||||
|
||||
@PostMapping("/sync-property-table")
|
||||
@Operation(summary = "同步产品属性表结构到 TDengine")
|
||||
@PreAuthorize("@ss.hasPermission('iot:product:update')")
|
||||
public CommonResult<Boolean> syncProductPropertyTable() {
|
||||
productService.syncProductPropertyTable();
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/simple-list")
|
||||
@Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项")
|
||||
@Parameter(name = "deviceType", description = "设备类型", example = "1")
|
||||
|
||||
@@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsSummaryRespVO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
|
||||
import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService;
|
||||
|
||||
@@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.mybatis.core.type.LongSetTypeHandler;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
@@ -108,10 +108,6 @@ public class IotDeviceDO extends TenantBaseDO {
|
||||
*/
|
||||
private LocalDateTime activeTime;
|
||||
|
||||
/**
|
||||
* 设备的 IP 地址
|
||||
*/
|
||||
private String ip;
|
||||
/**
|
||||
* 固件编号
|
||||
*
|
||||
|
||||
@@ -84,7 +84,7 @@ public class IotDeviceMessageDO {
|
||||
* 请求方法
|
||||
*
|
||||
* 枚举 {@link IotDeviceMessageMethodEnum}
|
||||
* 例如说:thing.property.report 属性上报
|
||||
* 例如说:thing.property.post 属性上报
|
||||
*/
|
||||
private String method;
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package cn.iocoder.yudao.module.iot.dal.dataobject.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* IoT 设备 Modbus 连接配置 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("iot_device_modbus_config")
|
||||
@KeySequence("iot_device_modbus_config_seq")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotDeviceModbusConfigDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 产品编号
|
||||
*
|
||||
* 关联 {@link IotProductDO#getId()}
|
||||
*/
|
||||
private Long productId;
|
||||
/**
|
||||
* 设备编号
|
||||
*
|
||||
* 关联 {@link IotDeviceDO#getId()}
|
||||
*/
|
||||
private Long deviceId;
|
||||
|
||||
/**
|
||||
* Modbus 服务器 IP 地址
|
||||
*/
|
||||
private String ip;
|
||||
/**
|
||||
* Modbus 服务器端口
|
||||
*/
|
||||
private Integer port;
|
||||
/**
|
||||
* 从站地址
|
||||
*/
|
||||
private Integer slaveId;
|
||||
/**
|
||||
* 连接超时时间,单位:毫秒
|
||||
*/
|
||||
private Integer timeout;
|
||||
/**
|
||||
* 重试间隔,单位:毫秒
|
||||
*/
|
||||
private Integer retryInterval;
|
||||
/**
|
||||
* 模式
|
||||
*
|
||||
* @see IotModbusModeEnum
|
||||
*/
|
||||
private Integer mode;
|
||||
/**
|
||||
* 数据帧格式
|
||||
*
|
||||
* @see IotModbusFrameFormatEnum
|
||||
*/
|
||||
private Integer frameFormat;
|
||||
/**
|
||||
* 状态
|
||||
*
|
||||
* 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package cn.iocoder.yudao.module.iot.dal.dataobject.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* IoT 设备 Modbus 点位配置 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("iot_device_modbus_point")
|
||||
@KeySequence("iot_device_modbus_point_seq")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotDeviceModbusPointDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 设备编号
|
||||
*
|
||||
* 关联 {@link IotDeviceDO#getId()}
|
||||
*/
|
||||
private Long deviceId;
|
||||
/**
|
||||
* 物模型属性编号
|
||||
*
|
||||
* 关联 {@link IotThingModelDO#getId()}
|
||||
*/
|
||||
private Long thingModelId;
|
||||
/**
|
||||
* 属性标识符
|
||||
*
|
||||
* 冗余 {@link IotThingModelDO#getIdentifier()}
|
||||
*/
|
||||
private String identifier;
|
||||
/**
|
||||
* 属性名称
|
||||
*
|
||||
* 冗余 {@link IotThingModelDO#getName()}
|
||||
*/
|
||||
private String name;
|
||||
|
||||
// ========== Modbus 协议配置 ==========
|
||||
|
||||
/**
|
||||
* Modbus 功能码
|
||||
*
|
||||
* 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器)
|
||||
*/
|
||||
private Integer functionCode;
|
||||
/**
|
||||
* 寄存器起始地址
|
||||
*/
|
||||
private Integer registerAddress;
|
||||
/**
|
||||
* 寄存器数量
|
||||
*/
|
||||
private Integer registerCount;
|
||||
/**
|
||||
* 字节序
|
||||
*
|
||||
* 枚举 {@link IotModbusByteOrderEnum}
|
||||
*/
|
||||
private String byteOrder;
|
||||
/**
|
||||
* 原始数据类型
|
||||
*
|
||||
* 枚举 {@link IotModbusRawDataTypeEnum}
|
||||
*/
|
||||
private String rawDataType;
|
||||
/**
|
||||
* 缩放因子
|
||||
*/
|
||||
private BigDecimal scale;
|
||||
/**
|
||||
* 轮询间隔(毫秒)
|
||||
*/
|
||||
private Integer pollInterval;
|
||||
/**
|
||||
* 状态
|
||||
*
|
||||
* 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.iocoder.yudao.module.iot.dal.mysql.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 设备 Modbus 连接配置 Mapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Mapper
|
||||
public interface IotDeviceModbusConfigMapper extends BaseMapperX<IotDeviceModbusConfigDO> {
|
||||
|
||||
default IotDeviceModbusConfigDO selectByDeviceId(Long deviceId) {
|
||||
return selectOne(IotDeviceModbusConfigDO::getDeviceId, deviceId);
|
||||
}
|
||||
|
||||
default List<IotDeviceModbusConfigDO> selectList(IotModbusDeviceConfigListReqDTO reqDTO) {
|
||||
return selectList(new LambdaQueryWrapperX<IotDeviceModbusConfigDO>()
|
||||
.eqIfPresent(IotDeviceModbusConfigDO::getStatus, reqDTO.getStatus())
|
||||
.eqIfPresent(IotDeviceModbusConfigDO::getMode, reqDTO.getMode())
|
||||
.inIfPresent(IotDeviceModbusConfigDO::getDeviceId, reqDTO.getDeviceIds()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.iocoder.yudao.module.iot.dal.mysql.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 设备 Modbus 点位配置 Mapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Mapper
|
||||
public interface IotDeviceModbusPointMapper extends BaseMapperX<IotDeviceModbusPointDO> {
|
||||
|
||||
default PageResult<IotDeviceModbusPointDO> selectPage(IotDeviceModbusPointPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<IotDeviceModbusPointDO>()
|
||||
.eqIfPresent(IotDeviceModbusPointDO::getDeviceId, reqVO.getDeviceId())
|
||||
.likeIfPresent(IotDeviceModbusPointDO::getIdentifier, reqVO.getIdentifier())
|
||||
.likeIfPresent(IotDeviceModbusPointDO::getName, reqVO.getName())
|
||||
.eqIfPresent(IotDeviceModbusPointDO::getFunctionCode, reqVO.getFunctionCode())
|
||||
.eqIfPresent(IotDeviceModbusPointDO::getStatus, reqVO.getStatus())
|
||||
.orderByDesc(IotDeviceModbusPointDO::getId));
|
||||
}
|
||||
|
||||
default List<IotDeviceModbusPointDO> selectListByDeviceIdsAndStatus(Collection<Long> deviceIds, Integer status) {
|
||||
return selectList(new LambdaQueryWrapperX<IotDeviceModbusPointDO>()
|
||||
.in(IotDeviceModbusPointDO::getDeviceId, deviceIds)
|
||||
.eq(IotDeviceModbusPointDO::getStatus, status));
|
||||
}
|
||||
|
||||
default IotDeviceModbusPointDO selectByDeviceIdAndIdentifier(Long deviceId, String identifier) {
|
||||
return selectOne(IotDeviceModbusPointDO::getDeviceId, deviceId,
|
||||
IotDeviceModbusPointDO::getIdentifier, identifier);
|
||||
}
|
||||
|
||||
default void updateByThingModelId(Long thingModelId, IotDeviceModbusPointDO updateObj) {
|
||||
update(updateObj, new LambdaQueryWrapperX<IotDeviceModbusPointDO>()
|
||||
.eq(IotDeviceModbusPointDO::getThingModelId, thingModelId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,6 +38,10 @@ public interface IotProductMapper extends BaseMapperX<IotProductDO> {
|
||||
.apply("LOWER(product_key) = {0}", productKey.toLowerCase()));
|
||||
}
|
||||
|
||||
default List<IotProductDO> selectListByStatus(Integer status) {
|
||||
return selectList(IotProductDO::getStatus, status);
|
||||
}
|
||||
|
||||
default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) {
|
||||
return selectCount(new LambdaQueryWrapperX<IotProductDO>()
|
||||
.geIfPresent(IotProductDO::getCreateTime, createTime));
|
||||
|
||||
@@ -54,6 +54,14 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在");
|
||||
ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除");
|
||||
|
||||
// ========== 设备 Modbus 配置 1-050-006-000 ==========
|
||||
ErrorCode DEVICE_MODBUS_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "设备 Modbus 连接配置不存在");
|
||||
ErrorCode DEVICE_MODBUS_CONFIG_EXISTS = new ErrorCode(1_050_006_001, "设备 Modbus 连接配置已存在");
|
||||
|
||||
// ========== 设备 Modbus 点位 1-050-007-000 ==========
|
||||
ErrorCode DEVICE_MODBUS_POINT_NOT_EXISTS = new ErrorCode(1_050_007_000, "设备 Modbus 点位配置不存在");
|
||||
ErrorCode DEVICE_MODBUS_POINT_EXISTS = new ErrorCode(1_050_007_001, "设备 Modbus 点位配置已存在");
|
||||
|
||||
// ========== OTA 固件相关 1-050-008-000 ==========
|
||||
|
||||
ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在");
|
||||
|
||||
@@ -19,9 +19,9 @@ public enum IotDataSinkTypeEnum implements ArrayValuable<Integer> {
|
||||
TCP(2, "TCP"),
|
||||
WEBSOCKET(3, "WebSocket"),
|
||||
|
||||
MQTT(10, "MQTT"), // TODO 待实现;
|
||||
MQTT(10, "MQTT"), // TODO @puhui999:待实现;
|
||||
|
||||
DATABASE(20, "Database"), // TODO @puhui999:待实现;可以简单点,对应的表名是什么,字段先固定了。
|
||||
DATABASE(20, "Database"), // TODO @puhui999:待实现;
|
||||
REDIS(21, "Redis"),
|
||||
|
||||
ROCKETMQ(30, "RocketMQ"),
|
||||
|
||||
@@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
|
||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import cn.iocoder.yudao.module.iot.framework.iot.config.YudaoIotProperties;
|
||||
|
||||
@@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
|
||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO;
|
||||
|
||||
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.mq.consumer.device;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
@@ -67,7 +67,6 @@ public class IotDeviceMessageSubscriber implements IotMessageSubscriber<IotDevic
|
||||
IotDeviceDO device = deviceService.validateDeviceExistsFromCache(message.getDeviceId());
|
||||
devicePropertyService.updateDeviceReportTimeAsync(device.getId(), LocalDateTime.now());
|
||||
// 1.2 更新设备的连接 server
|
||||
// TODO 芋艿:HTTP 网关的上行消息,不应该更新 serverId,会覆盖掉 MQTT 等长连接的 serverId,导致下行消息无法发送。
|
||||
devicePropertyService.updateDeviceServerIdAsync(device.getId(), message.getServerId());
|
||||
|
||||
// 2. 未上线的设备,强制上线
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.springframework.stereotype.Component;
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class IotDataRuleMessageHandler implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
public class IotDataRuleMessageSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
|
||||
@Resource
|
||||
private IotDataRuleService dataRuleService;
|
||||
@@ -9,7 +9,6 @@ import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
// TODO @puhui999:后面重构哈
|
||||
/**
|
||||
* 针对 {@link IotDeviceMessage} 的消费者,处理规则场景
|
||||
*
|
||||
@@ -17,7 +16,7 @@ import org.springframework.stereotype.Component;
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class IotSceneRuleMessageHandler implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
public class IotSceneRuleMessageSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
|
||||
@Resource
|
||||
private IotSceneRuleService sceneRuleService;
|
||||
@@ -0,0 +1,48 @@
|
||||
package cn.iocoder.yudao.module.iot.service.device;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 设备 Modbus 连接配置 Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface IotDeviceModbusConfigService {
|
||||
|
||||
/**
|
||||
* 保存设备 Modbus 连接配置(新增或更新)
|
||||
*
|
||||
* @param saveReqVO 保存信息
|
||||
*/
|
||||
void saveDeviceModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO saveReqVO);
|
||||
|
||||
/**
|
||||
* 获得设备 Modbus 连接配置
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 设备 Modbus 连接配置
|
||||
*/
|
||||
IotDeviceModbusConfigDO getDeviceModbusConfig(Long id);
|
||||
|
||||
/**
|
||||
* 根据设备编号获得 Modbus 连接配置
|
||||
*
|
||||
* @param deviceId 设备编号
|
||||
* @return 设备 Modbus 连接配置
|
||||
*/
|
||||
IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId);
|
||||
|
||||
/**
|
||||
* 获得 Modbus 连接配置列表
|
||||
*
|
||||
* @param listReqDTO 查询参数
|
||||
* @return Modbus 连接配置列表
|
||||
*/
|
||||
List<IotDeviceModbusConfigDO> getDeviceModbusConfigList(IotModbusDeviceConfigListReqDTO listReqDTO);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package cn.iocoder.yudao.module.iot.service.device;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusConfigMapper;
|
||||
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 设备 Modbus 连接配置 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigService {
|
||||
|
||||
@Resource
|
||||
private IotDeviceModbusConfigMapper modbusConfigMapper;
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
@Resource
|
||||
private IotProductService productService;
|
||||
|
||||
@Override
|
||||
public void saveDeviceModbusConfig(IotDeviceModbusConfigSaveReqVO saveReqVO) {
|
||||
// 1.1 校验设备存在
|
||||
IotDeviceDO device = deviceService.validateDeviceExists(saveReqVO.getDeviceId());
|
||||
// 1.2 根据产品 protocolType 条件校验
|
||||
IotProductDO product = productService.getProduct(device.getProductId());
|
||||
Assert.notNull(product, "产品不存在");
|
||||
validateModbusConfigByProtocolType(saveReqVO, product.getProtocolType());
|
||||
|
||||
// 2. 根据数据库中是否已有配置,决定是新增还是更新
|
||||
IotDeviceModbusConfigDO existConfig = modbusConfigMapper.selectByDeviceId(saveReqVO.getDeviceId());
|
||||
if (existConfig == null) {
|
||||
IotDeviceModbusConfigDO modbusConfig = BeanUtils.toBean(saveReqVO, IotDeviceModbusConfigDO.class);
|
||||
modbusConfigMapper.insert(modbusConfig);
|
||||
} else {
|
||||
IotDeviceModbusConfigDO updateObj = BeanUtils.toBean(saveReqVO, IotDeviceModbusConfigDO.class,
|
||||
o -> o.setId(existConfig.getId()));
|
||||
modbusConfigMapper.updateById(updateObj);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceModbusConfigDO getDeviceModbusConfig(Long id) {
|
||||
return modbusConfigMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId) {
|
||||
return modbusConfigMapper.selectByDeviceId(deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IotDeviceModbusConfigDO> getDeviceModbusConfigList(IotModbusDeviceConfigListReqDTO listReqDTO) {
|
||||
return modbusConfigMapper.selectList(listReqDTO);
|
||||
}
|
||||
|
||||
private void validateModbusConfigByProtocolType(IotDeviceModbusConfigSaveReqVO saveReqVO, String protocolType) {
|
||||
IotProtocolTypeEnum protocolTypeEnum = IotProtocolTypeEnum.of(protocolType);
|
||||
if (protocolTypeEnum == null) {
|
||||
return;
|
||||
}
|
||||
if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_CLIENT) {
|
||||
Assert.isTrue(StrUtil.isNotEmpty(saveReqVO.getIp()), "Client 模式下,IP 地址不能为空");
|
||||
Assert.notNull(saveReqVO.getPort(), "Client 模式下,端口不能为空");
|
||||
Assert.notNull(saveReqVO.getTimeout(), "Client 模式下,连接超时时间不能为空");
|
||||
Assert.notNull(saveReqVO.getRetryInterval(), "Client 模式下,重试间隔不能为空");
|
||||
} else if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_SERVER) {
|
||||
Assert.notNull(saveReqVO.getMode(), "Server 模式下,工作模式不能为空");
|
||||
Assert.notNull(saveReqVO.getFrameFormat(), "Server 模式下,数据帧格式不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package cn.iocoder.yudao.module.iot.service.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 设备 Modbus 点位配置 Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface IotDeviceModbusPointService {
|
||||
|
||||
/**
|
||||
* 创建设备 Modbus 点位配置
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createDeviceModbusPoint(@Valid IotDeviceModbusPointSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新设备 Modbus 点位配置
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateDeviceModbusPoint(@Valid IotDeviceModbusPointSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 删除设备 Modbus 点位配置
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteDeviceModbusPoint(Long id);
|
||||
|
||||
/**
|
||||
* 获得设备 Modbus 点位配置
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 设备 Modbus 点位配置
|
||||
*/
|
||||
IotDeviceModbusPointDO getDeviceModbusPoint(Long id);
|
||||
|
||||
/**
|
||||
* 获得设备 Modbus 点位配置分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 设备 Modbus 点位配置分页
|
||||
*/
|
||||
PageResult<IotDeviceModbusPointDO> getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 物模型变更时,更新关联点位的冗余字段(identifier、name)
|
||||
*
|
||||
* @param thingModelId 物模型编号
|
||||
* @param identifier 物模型标识符
|
||||
* @param name 物模型名称
|
||||
*/
|
||||
void updateDeviceModbusPointByThingModel(Long thingModelId, String identifier, String name);
|
||||
|
||||
/**
|
||||
* 根据设备编号批量获得启用的点位配置 Map
|
||||
*
|
||||
* @param deviceIds 设备编号集合
|
||||
* @return 设备点位 Map,key 为设备编号,value 为点位配置列表
|
||||
*/
|
||||
Map<Long, List<IotDeviceModbusPointDO>> getEnabledDeviceModbusPointMapByDeviceIds(Collection<Long> deviceIds);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package cn.iocoder.yudao.module.iot.service.device;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusPointMapper;
|
||||
import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
|
||||
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* IoT 设备 Modbus 点位配置 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointService {
|
||||
|
||||
@Resource
|
||||
private IotDeviceModbusPointMapper modbusPointMapper;
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
|
||||
@Resource
|
||||
private IotThingModelService thingModelService;
|
||||
|
||||
@Override
|
||||
public Long createDeviceModbusPoint(IotDeviceModbusPointSaveReqVO createReqVO) {
|
||||
// 1.1 校验设备存在
|
||||
deviceService.validateDeviceExists(createReqVO.getDeviceId());
|
||||
// 1.2 校验物模型属性存在
|
||||
IotThingModelDO thingModel = validateThingModelExists(createReqVO.getThingModelId());
|
||||
// 1.3 校验同一设备下点位唯一性(基于 identifier)
|
||||
validateDeviceModbusPointUnique(createReqVO.getDeviceId(), thingModel.getIdentifier(), null);
|
||||
|
||||
// 2. 插入
|
||||
IotDeviceModbusPointDO modbusPoint = BeanUtils.toBean(createReqVO, IotDeviceModbusPointDO.class,
|
||||
o -> o.setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName()));
|
||||
modbusPointMapper.insert(modbusPoint);
|
||||
return modbusPoint.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDeviceModbusPoint(IotDeviceModbusPointSaveReqVO updateReqVO) {
|
||||
// 1.1 校验存在
|
||||
validateDeviceModbusPointExists(updateReqVO.getId());
|
||||
// 1.2 校验设备存在
|
||||
deviceService.validateDeviceExists(updateReqVO.getDeviceId());
|
||||
// 1.3 校验物模型属性存在
|
||||
IotThingModelDO thingModel = validateThingModelExists(updateReqVO.getThingModelId());
|
||||
// 1.4 校验同一设备下点位唯一性
|
||||
validateDeviceModbusPointUnique(updateReqVO.getDeviceId(), thingModel.getIdentifier(), updateReqVO.getId());
|
||||
|
||||
// 2. 更新
|
||||
IotDeviceModbusPointDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusPointDO.class,
|
||||
o -> o.setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName()));
|
||||
modbusPointMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDeviceModbusPointByThingModel(Long thingModelId, String identifier, String name) {
|
||||
IotDeviceModbusPointDO updateObj = new IotDeviceModbusPointDO()
|
||||
.setIdentifier(identifier).setName(name);
|
||||
modbusPointMapper.updateByThingModelId(thingModelId, updateObj);
|
||||
}
|
||||
|
||||
private IotThingModelDO validateThingModelExists(Long id) {
|
||||
IotThingModelDO thingModel = thingModelService.getThingModel(id);
|
||||
if (thingModel == null) {
|
||||
throw exception(THING_MODEL_NOT_EXISTS);
|
||||
}
|
||||
return thingModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDeviceModbusPoint(Long id) {
|
||||
// 校验存在
|
||||
validateDeviceModbusPointExists(id);
|
||||
// 删除
|
||||
modbusPointMapper.deleteById(id);
|
||||
}
|
||||
|
||||
private void validateDeviceModbusPointExists(Long id) {
|
||||
IotDeviceModbusPointDO point = modbusPointMapper.selectById(id);
|
||||
if (point == null) {
|
||||
throw exception(DEVICE_MODBUS_POINT_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDeviceModbusPointUnique(Long deviceId, String identifier, Long excludeId) {
|
||||
IotDeviceModbusPointDO point = modbusPointMapper.selectByDeviceIdAndIdentifier(deviceId, identifier);
|
||||
if (point != null && ObjUtil.notEqual(point.getId(), excludeId)) {
|
||||
throw exception(DEVICE_MODBUS_POINT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceModbusPointDO getDeviceModbusPoint(Long id) {
|
||||
return modbusPointMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<IotDeviceModbusPointDO> getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO) {
|
||||
return modbusPointMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, List<IotDeviceModbusPointDO>> getEnabledDeviceModbusPointMapByDeviceIds(Collection<Long> deviceIds) {
|
||||
if (CollUtil.isEmpty(deviceIds)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<IotDeviceModbusPointDO> pointList = modbusPointMapper.selectListByDeviceIdsAndStatus(
|
||||
deviceIds, CommonStatusEnum.ENABLE.getStatus());
|
||||
return convertMultiMap(pointList, IotDeviceModbusPointDO::getDeviceId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
|
||||
@@ -17,7 +17,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
|
||||
@@ -3,11 +3,15 @@ package cn.iocoder.yudao.module.iot.service.ota;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record.IotOtaTaskRecordPageReqVO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.ota.IotDeviceOtaProgressReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.ota.IotDeviceOtaUpgradeReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO;
|
||||
@@ -133,9 +137,9 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService {
|
||||
public boolean pushOtaTaskRecord(IotOtaTaskRecordDO record, IotOtaFirmwareDO fireware, IotDeviceDO device) {
|
||||
try {
|
||||
// 1. 推送 OTA 任务记录
|
||||
IotDeviceMessage message = IotDeviceMessage.buildOtaUpgrade(
|
||||
fireware.getVersion(), fireware.getFileUrl(), fireware.getFileSize(),
|
||||
fireware.getFileDigestAlgorithm(), fireware.getFileDigestValue());
|
||||
IotDeviceOtaUpgradeReqDTO params = BeanUtils.toBean(fireware, IotDeviceOtaUpgradeReqDTO.class);
|
||||
IotDeviceMessage message = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), params);
|
||||
deviceMessageService.sendDeviceMessage(message, device);
|
||||
|
||||
// 2. 更新 OTA 升级记录状态为进行中
|
||||
@@ -163,17 +167,16 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService {
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@SuppressWarnings("unchecked")
|
||||
public void updateOtaRecordProgress(IotDeviceDO device, IotDeviceMessage message) {
|
||||
// 1.1 参数解析
|
||||
Map<String, Object> params = (Map<String, Object>) message.getParams();
|
||||
String version = MapUtil.getStr(params, "version");
|
||||
IotDeviceOtaProgressReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceOtaProgressReqDTO.class);
|
||||
String version = params.getVersion();
|
||||
Assert.notBlank(version, "version 不能为空");
|
||||
Integer status = MapUtil.getInt(params, "status");
|
||||
Integer status = params.getStatus();
|
||||
Assert.notNull(status, "status 不能为空");
|
||||
Assert.notNull(IotOtaTaskRecordStatusEnum.of(status), "status 状态不正确");
|
||||
String description = MapUtil.getStr(params, "description");
|
||||
Integer progress = MapUtil.getInt(params, "progress");
|
||||
String description = params.getDescription();
|
||||
Integer progress = params.getProgress();
|
||||
Assert.notNull(progress, "progress 不能为空");
|
||||
Assert.isTrue(progress >= 0 && progress <= 100, "progress 必须在 0-100 之间");
|
||||
// 1.2 查询 OTA 升级记录
|
||||
|
||||
@@ -10,6 +10,9 @@ import javax.annotation.Nullable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
|
||||
|
||||
/**
|
||||
* IoT 产品 Service 接口
|
||||
@@ -121,6 +124,24 @@ public interface IotProductService {
|
||||
*/
|
||||
Long getProductCount(@Nullable LocalDateTime createTime);
|
||||
|
||||
/**
|
||||
* 批量获得产品列表
|
||||
*
|
||||
* @param ids 产品编号集合
|
||||
* @return 产品列表
|
||||
*/
|
||||
List<IotProductDO> getProductList(Collection<Long> ids);
|
||||
|
||||
/**
|
||||
* 批量获得产品 Map
|
||||
*
|
||||
* @param ids 产品编号集合
|
||||
* @return 产品 Map(key: 产品编号, value: 产品)
|
||||
*/
|
||||
default Map<Long, IotProductDO> getProductMap(Collection<Long> ids) {
|
||||
return convertMap(getProductList(ids), IotProductDO::getId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量校验产品存在
|
||||
*
|
||||
@@ -128,4 +149,11 @@ public interface IotProductService {
|
||||
*/
|
||||
void validateProductsExist(Collection<Long> ids);
|
||||
|
||||
/**
|
||||
* 同步产品的 TDengine 表结构
|
||||
*
|
||||
* 目的:当 MySQL 和 TDengine 不同步时,强制将已发布产品的表结构同步到 TDengine 中
|
||||
*/
|
||||
void syncProductPropertyTable();
|
||||
|
||||
}
|
||||
@@ -15,8 +15,10 @@ import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
|
||||
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
@@ -33,6 +35,7 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
|
||||
*
|
||||
* @author ahh
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Validated
|
||||
public class IotProductServiceImpl implements IotProductService {
|
||||
@@ -40,10 +43,11 @@ public class IotProductServiceImpl implements IotProductService {
|
||||
@Resource
|
||||
private IotProductMapper productMapper;
|
||||
|
||||
@Resource
|
||||
private IotDevicePropertyService devicePropertyDataService;
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,避免循环依赖
|
||||
private IotDevicePropertyService devicePropertyDataService;
|
||||
|
||||
@Override
|
||||
public Long createProduct(IotProductSaveReqVO createReqVO) {
|
||||
@@ -171,6 +175,32 @@ public class IotProductServiceImpl implements IotProductService {
|
||||
return productMapper.selectCountByCreateTime(createTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IotProductDO> getProductList(Collection<Long> ids) {
|
||||
return productMapper.selectByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncProductPropertyTable() {
|
||||
// 1. 获取所有已发布的产品
|
||||
List<IotProductDO> products = productMapper.selectListByStatus(
|
||||
IotProductStatusEnum.PUBLISHED.getStatus());
|
||||
log.info("[syncProductPropertyTable][开始同步,已发布产品数量({})]", products.size());
|
||||
|
||||
// 2. 遍历同步 TDengine 表结构(创建产品超级表数据模型)
|
||||
int successCount = 0;
|
||||
for (IotProductDO product : products) {
|
||||
try {
|
||||
devicePropertyDataService.defineDevicePropertyData(product.getId());
|
||||
successCount++;
|
||||
log.info("[syncProductPropertyTable][产品({}/{}) 同步成功]", product.getId(), product.getName());
|
||||
} catch (Exception e) {
|
||||
log.error("[syncProductPropertyTable][产品({}/{}) 同步失败]", product.getId(), product.getName(), e);
|
||||
}
|
||||
}
|
||||
log.info("[syncProductPropertyTable][同步完成,成功({}/{})个]", successCount, products.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateProductsExist(Collection<Long> ids) {
|
||||
if (CollUtil.isEmpty(ids)) {
|
||||
|
||||
@@ -14,8 +14,6 @@ import java.time.Duration;
|
||||
|
||||
// TODO @芋艿:数据库
|
||||
// TODO @芋艿:mqtt
|
||||
// TODO @芋艿:tcp
|
||||
// TODO @芋艿:websocket
|
||||
|
||||
/**
|
||||
* 可缓存的 {@link IotDataRuleAction} 抽象实现
|
||||
|
||||
@@ -15,7 +15,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 设备属性设置的 {@link IotSceneRuleAction} 实现类
|
||||
@@ -24,7 +23,7 @@ import java.util.Map;
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction {
|
||||
public class IotDevicePropertySetSceneRuleAction implements IotSceneRuleAction {
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
@@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper;
|
||||
import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
|
||||
import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService;
|
||||
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -50,6 +51,9 @@ public class IotThingModelServiceImpl implements IotThingModelService {
|
||||
@Resource
|
||||
@Lazy // 延迟加载,解决循环依赖
|
||||
private IotProductService productService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,解决循环依赖
|
||||
private IotDeviceModbusPointService deviceModbusPointService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -84,7 +88,11 @@ public class IotThingModelServiceImpl implements IotThingModelService {
|
||||
IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO);
|
||||
thingModelMapper.updateById(thingModel);
|
||||
|
||||
// 3. 删除缓存
|
||||
// 3. 同步更新 Modbus 点位的冗余字段(identifier、name)
|
||||
deviceModbusPointService.updateDeviceModbusPointByThingModel(
|
||||
updateReqVO.getId(), updateReqVO.getIdentifier(), updateReqVO.getName());
|
||||
|
||||
// 4. 删除缓存
|
||||
deleteThingModelListCache(updateReqVO.getProductId());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.service.rule.scene;
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package cn.iocoder.yudao.module.iot.core.biz;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.*;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
|
||||
@@ -50,4 +47,12 @@ public interface IotDeviceCommonApi {
|
||||
*/
|
||||
CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 获取 Modbus 设备配置列表
|
||||
*
|
||||
* @param listReqDTO 查询参数
|
||||
* @return Modbus 设备配置列表
|
||||
*/
|
||||
CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList(IotModbusDeviceConfigListReqDTO listReqDTO);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.module.iot.core.biz.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* IoT Modbus 设备配置列表查询 Request DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class IotModbusDeviceConfigListReqDTO {
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 模式
|
||||
*/
|
||||
private Integer mode;
|
||||
|
||||
/**
|
||||
* 协议类型
|
||||
*/
|
||||
private String protocolType;
|
||||
|
||||
/**
|
||||
* 设备 ID 集合
|
||||
*/
|
||||
private Set<Long> deviceIds;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package cn.iocoder.yudao.module.iot.core.biz.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT Modbus 设备配置 Response DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class IotModbusDeviceConfigRespDTO {
|
||||
|
||||
/**
|
||||
* 设备编号
|
||||
*/
|
||||
private Long deviceId;
|
||||
/**
|
||||
* 产品标识
|
||||
*/
|
||||
private String productKey;
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
// ========== Modbus 连接配置 ==========
|
||||
|
||||
/**
|
||||
* Modbus 服务器 IP 地址
|
||||
*/
|
||||
private String ip;
|
||||
/**
|
||||
* Modbus 服务器端口
|
||||
*/
|
||||
private Integer port;
|
||||
/**
|
||||
* 从站地址
|
||||
*/
|
||||
private Integer slaveId;
|
||||
/**
|
||||
* 连接超时时间,单位:毫秒
|
||||
*/
|
||||
private Integer timeout;
|
||||
/**
|
||||
* 重试间隔,单位:毫秒
|
||||
*/
|
||||
private Integer retryInterval;
|
||||
/**
|
||||
* 模式
|
||||
*/
|
||||
private Integer mode;
|
||||
/**
|
||||
* 数据帧格式
|
||||
*/
|
||||
private Integer frameFormat;
|
||||
|
||||
// ========== Modbus 点位配置 ==========
|
||||
|
||||
/**
|
||||
* 点位列表
|
||||
*/
|
||||
private List<IotModbusPointRespDTO> points;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package cn.iocoder.yudao.module.iot.core.biz.dto;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* IoT Modbus 点位配置 Response DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class IotModbusPointRespDTO {
|
||||
|
||||
/**
|
||||
* 点位编号
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 属性标识符(物模型的 identifier)
|
||||
*/
|
||||
private String identifier;
|
||||
/**
|
||||
* 属性名称(物模型的 name)
|
||||
*/
|
||||
private String name;
|
||||
|
||||
// ========== Modbus 协议配置 ==========
|
||||
|
||||
/**
|
||||
* Modbus 功能码
|
||||
*
|
||||
* 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器)
|
||||
*/
|
||||
private Integer functionCode;
|
||||
/**
|
||||
* 寄存器起始地址
|
||||
*/
|
||||
private Integer registerAddress;
|
||||
/**
|
||||
* 寄存器数量
|
||||
*/
|
||||
private Integer registerCount;
|
||||
/**
|
||||
* 字节序
|
||||
*
|
||||
* 枚举 {@link IotModbusByteOrderEnum}
|
||||
*/
|
||||
private String byteOrder;
|
||||
/**
|
||||
* 原始数据类型
|
||||
*
|
||||
* 枚举 {@link IotModbusRawDataTypeEnum}
|
||||
*/
|
||||
private String rawDataType;
|
||||
/**
|
||||
* 缩放因子
|
||||
*/
|
||||
private BigDecimal scale;
|
||||
/**
|
||||
* 轮询间隔(毫秒)
|
||||
*/
|
||||
private Integer pollInterval;
|
||||
|
||||
}
|
||||
@@ -64,7 +64,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> {
|
||||
// ========== OTA 固件 ==========
|
||||
// 可参考:https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates
|
||||
|
||||
OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false),
|
||||
OTA_UPGRADE("thing.ota.upgrade", "OTA 固件信息推送", false),
|
||||
OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true),
|
||||
|
||||
;
|
||||
|
||||
@@ -25,7 +25,8 @@ public enum IotProtocolTypeEnum implements ArrayValuable<String> {
|
||||
MQTT("mqtt"),
|
||||
EMQX("emqx"),
|
||||
COAP("coap"),
|
||||
MODBUS_TCP("modbus_tcp");
|
||||
MODBUS_TCP_CLIENT("modbus_tcp_client"),
|
||||
MODBUS_TCP_SERVER("modbus_tcp_server");
|
||||
|
||||
public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.iocoder.yudao.module.iot.core.enums;
|
||||
package cn.iocoder.yudao.module.iot.core.enums.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
|
||||
import lombok.Getter;
|
||||
@@ -0,0 +1,54 @@
|
||||
package cn.iocoder.yudao.module.iot.core.enums.modbus;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* IoT Modbus 字节序枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum IotModbusByteOrderEnum implements ArrayValuable<String> {
|
||||
|
||||
AB("AB", "大端序(16位)", 2),
|
||||
BA("BA", "小端序(16位)", 2),
|
||||
ABCD("ABCD", "大端序(32位)", 4),
|
||||
CDAB("CDAB", "大端字交换(32位)", 4),
|
||||
DCBA("DCBA", "小端序(32位)", 4),
|
||||
BADC("BADC", "小端字交换(32位)", 4);
|
||||
|
||||
public static final String[] ARRAYS = Arrays.stream(values())
|
||||
.map(IotModbusByteOrderEnum::getOrder)
|
||||
.toArray(String[]::new);
|
||||
|
||||
/**
|
||||
* 字节序
|
||||
*/
|
||||
private final String order;
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
private final String name;
|
||||
/**
|
||||
* 字节数
|
||||
*/
|
||||
private final Integer byteCount;
|
||||
|
||||
@Override
|
||||
public String[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
public static IotModbusByteOrderEnum getByOrder(String order) {
|
||||
return Arrays.stream(values())
|
||||
.filter(e -> e.getOrder().equals(order))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.iocoder.yudao.module.iot.core.enums.modbus;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* IoT Modbus 数据帧格式枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum IotModbusFrameFormatEnum implements ArrayValuable<Integer> {
|
||||
|
||||
MODBUS_TCP(1),
|
||||
MODBUS_RTU(2);
|
||||
|
||||
public static final Integer[] ARRAYS = Arrays.stream(values())
|
||||
.map(IotModbusFrameFormatEnum::getFormat)
|
||||
.toArray(Integer[]::new);
|
||||
|
||||
/**
|
||||
* 格式
|
||||
*/
|
||||
private final Integer format;
|
||||
|
||||
@Override
|
||||
public Integer[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cn.iocoder.yudao.module.iot.core.enums.modbus;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* IoT Modbus 工作模式枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum IotModbusModeEnum implements ArrayValuable<Integer> {
|
||||
|
||||
POLLING(1, "云端轮询"),
|
||||
ACTIVE_REPORT(2, "边缘采集");
|
||||
|
||||
public static final Integer[] ARRAYS = Arrays.stream(values())
|
||||
.map(IotModbusModeEnum::getMode)
|
||||
.toArray(Integer[]::new);
|
||||
|
||||
/**
|
||||
* 工作模式
|
||||
*/
|
||||
private final Integer mode;
|
||||
/**
|
||||
* 模式名称
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
@Override
|
||||
public Integer[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package cn.iocoder.yudao.module.iot.core.enums.modbus;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* IoT Modbus 原始数据类型枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum IotModbusRawDataTypeEnum implements ArrayValuable<String> {
|
||||
|
||||
INT16("INT16", "有符号 16 位整数", 1),
|
||||
UINT16("UINT16", "无符号 16 位整数", 1),
|
||||
INT32("INT32", "有符号 32 位整数", 2),
|
||||
UINT32("UINT32", "无符号 32 位整数", 2),
|
||||
FLOAT("FLOAT", "32 位浮点数", 2),
|
||||
DOUBLE("DOUBLE", "64 位浮点数", 4),
|
||||
BOOLEAN("BOOLEAN", "布尔值(用于线圈)", 1),
|
||||
STRING("STRING", "字符串", null); // null 表示可变长度
|
||||
|
||||
public static final String[] ARRAYS = Arrays.stream(values())
|
||||
.map(IotModbusRawDataTypeEnum::getType)
|
||||
.toArray(String[]::new);
|
||||
|
||||
/**
|
||||
* 数据类型
|
||||
*/
|
||||
private final String type;
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
private final String name;
|
||||
/**
|
||||
* 寄存器数量(null 表示可变)
|
||||
*/
|
||||
private final Integer registerCount;
|
||||
|
||||
@Override
|
||||
public String[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
public static IotModbusRawDataTypeEnum getByType(String type) {
|
||||
return Arrays.stream(values())
|
||||
.filter(e -> e.getType().equals(type))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package cn.iocoder.yudao.module.iot.core.mq.message;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.state.IotDeviceStateUpdateReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@@ -72,7 +72,7 @@ public class IotDeviceMessage {
|
||||
* 请求方法
|
||||
*
|
||||
* 枚举 {@link IotDeviceMessageMethodEnum}
|
||||
* 例如说:thing.property.report 属性上报
|
||||
* 例如说:thing.property.post 属性上报
|
||||
*/
|
||||
private String method;
|
||||
/**
|
||||
@@ -149,20 +149,12 @@ public class IotDeviceMessage {
|
||||
|
||||
public static IotDeviceMessage buildStateUpdateOnline() {
|
||||
return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(),
|
||||
MapUtil.of("state", IotDeviceStateEnum.ONLINE.getState()));
|
||||
new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.ONLINE.getState()));
|
||||
}
|
||||
|
||||
public static IotDeviceMessage buildStateOffline() {
|
||||
return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(),
|
||||
MapUtil.of("state", IotDeviceStateEnum.OFFLINE.getState()));
|
||||
}
|
||||
|
||||
public static IotDeviceMessage buildOtaUpgrade(String version, String fileUrl, Long fileSize,
|
||||
String fileDigestAlgorithm, String fileDigestValue) {
|
||||
return requestOf(IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), MapUtil.builder()
|
||||
.put("version", version).put("fileUrl", fileUrl).put("fileSize", fileSize)
|
||||
.put("fileDigestAlgorithm", fileDigestAlgorithm).put("fileDigestValue", fileDigestValue)
|
||||
.build());
|
||||
new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.OFFLINE.getState()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.auth;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT 设备动态注册 Request DTO
|
||||
* <p>
|
||||
* 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} 消息的 params 参数
|
||||
* <p>
|
||||
* 直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.auth;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -7,7 +8,7 @@ import lombok.NoArgsConstructor;
|
||||
/**
|
||||
* IoT 设备动态注册 Response DTO
|
||||
* <p>
|
||||
* 用于直连设备/网关的一型一密动态注册响应
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} 响应的设备信息
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.auth;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT 子设备动态注册 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.auth.register.sub 消息的 params 数组元素
|
||||
*
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} 消息的 params 数组元素
|
||||
* <p>
|
||||
* 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。
|
||||
*
|
||||
* @author 芋道源码
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.auth;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -7,7 +8,7 @@ import lombok.NoArgsConstructor;
|
||||
/**
|
||||
* IoT 子设备动态注册 Response DTO
|
||||
* <p>
|
||||
* 用于 thing.auth.register.sub 响应的设备信息
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} 响应的设备信息
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.config;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* IoT 设备配置推送 Request DTO
|
||||
* <p>
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#CONFIG_PUSH} 下行消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1">阿里云 - 远程配置</a>
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotDeviceConfigPushReqDTO {
|
||||
|
||||
/**
|
||||
* 配置编号
|
||||
*/
|
||||
private String configId;
|
||||
|
||||
/**
|
||||
* 配置文件大小(字节)
|
||||
*/
|
||||
private Long configSize;
|
||||
|
||||
/**
|
||||
* 签名方法
|
||||
*/
|
||||
private String signMethod;
|
||||
|
||||
/**
|
||||
* 签名
|
||||
*/
|
||||
private String sign;
|
||||
|
||||
/**
|
||||
* 配置文件下载地址
|
||||
*/
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 获取类型
|
||||
* <p>
|
||||
* file: 文件
|
||||
* content: 内容
|
||||
*/
|
||||
private String getType;
|
||||
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.event;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT 设备事件上报 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.event.post 消息的 params 参数
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#EVENT_POST} 消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-events">阿里云 - 设备上报事件</a>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.ota;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* IoT 设备 OTA 升级进度上报 Request DTO
|
||||
* <p>
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#OTA_PROGRESS} 上行消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates">阿里云 - OTA 升级</a>
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotDeviceOtaProgressReqDTO {
|
||||
|
||||
/**
|
||||
* 固件版本号
|
||||
*/
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* 升级状态
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 描述信息
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 升级进度(0-100)
|
||||
*/
|
||||
private Integer progress;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.ota;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* IoT 设备 OTA 固件升级推送 Request DTO
|
||||
* <p>
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#OTA_UPGRADE} 下行消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates">阿里云 - OTA 升级</a>
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotDeviceOtaUpgradeReqDTO {
|
||||
|
||||
/**
|
||||
* 固件版本号
|
||||
*/
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* 固件文件下载地址
|
||||
*/
|
||||
private String fileUrl;
|
||||
|
||||
/**
|
||||
* 固件文件大小(字节)
|
||||
*/
|
||||
private Long fileSize;
|
||||
|
||||
/**
|
||||
* 固件文件摘要算法
|
||||
*/
|
||||
private String fileDigestAlgorithm;
|
||||
|
||||
/**
|
||||
* 固件文件摘要值
|
||||
*/
|
||||
private String fileDigestValue;
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.property;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -9,7 +10,7 @@ import java.util.Map;
|
||||
/**
|
||||
* IoT 设备属性批量上报 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.event.property.pack.post 消息的 params 参数
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_PACK_POST} 消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="http://help.aliyun.com/zh/marketplace/gateway-reports-data-in-batches">阿里云 - 网关批量上报数据</a>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.property;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 设备属性上报 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.property.post 消息的 params 参数
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_POST} 消息的 params 参数
|
||||
* <p>
|
||||
* 本质是一个 Map,key 为属性标识符,value 为属性值
|
||||
*
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.property;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 设备属性设置 Request DTO
|
||||
* <p>
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_SET} 下行消息的 params 参数
|
||||
* <p>
|
||||
* 本质是一个 Map,key 为属性标识符,value 为属性值
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class IotDevicePropertySetReqDTO extends HashMap<String, Object> {
|
||||
|
||||
public IotDevicePropertySetReqDTO() {
|
||||
super();
|
||||
}
|
||||
|
||||
public IotDevicePropertySetReqDTO(Map<String, Object> properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建属性设置 DTO
|
||||
*
|
||||
* @param properties 属性数据
|
||||
* @return DTO 对象
|
||||
*/
|
||||
public static IotDevicePropertySetReqDTO of(Map<String, Object> properties) {
|
||||
return new IotDevicePropertySetReqDTO(properties);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.service;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 设备服务调用 Request DTO
|
||||
* <p>
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#SERVICE_INVOKE} 下行消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotDeviceServiceInvokeReqDTO {
|
||||
|
||||
/**
|
||||
* 服务标识符
|
||||
*/
|
||||
private String identifier;
|
||||
|
||||
/**
|
||||
* 服务输入参数
|
||||
*/
|
||||
private Map<String, Object> inputParams;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.state;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* IoT 设备状态更新 Request DTO
|
||||
* <p>
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#STATE_UPDATE} 消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotDeviceStateUpdateReqDTO {
|
||||
|
||||
/**
|
||||
* 设备状态
|
||||
*/
|
||||
private Integer state;
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.topo;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -9,7 +10,7 @@ import java.util.List;
|
||||
/**
|
||||
* IoT 设备拓扑添加 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.topo.add 消息的 params 参数
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#TOPO_ADD} 消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="http://help.aliyun.com/zh/marketplace/add-topological-relationship">阿里云 - 添加拓扑关系</a>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.topo;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@@ -10,7 +11,7 @@ import java.util.List;
|
||||
/**
|
||||
* IoT 设备拓扑关系变更通知 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.topo.change 下行消息的 params 参数
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#TOPO_CHANGE} 下行消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.topo;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
@@ -10,7 +11,7 @@ import java.util.List;
|
||||
/**
|
||||
* IoT 设备拓扑删除 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.topo.delete 消息的 params 参数
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#TOPO_DELETE} 消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/marketplace/delete-a-topological-relationship">阿里云 - 删除拓扑关系</a>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.topo;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT 设备拓扑关系获取 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展)
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#TOPO_GET} 请求的 params 参数(目前为空,预留扩展)
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.topo;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -8,7 +9,7 @@ import java.util.List;
|
||||
/**
|
||||
* IoT 设备拓扑关系获取 Response DTO
|
||||
* <p>
|
||||
* 用于 thing.topo.get 响应
|
||||
* 用于 {@link IotDeviceMessageMethodEnum#TOPO_GET} 响应
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
|
||||
|
||||
@@ -25,6 +25,14 @@ public class IotDeviceAuthUtils {
|
||||
return String.format("%s.%s", productKey, deviceName);
|
||||
}
|
||||
|
||||
public static String buildClientIdFromUsername(String username) {
|
||||
IotDeviceIdentity identity = parseUsername(username);
|
||||
if (identity == null) {
|
||||
return null;
|
||||
}
|
||||
return buildClientId(identity.getProductKey(), identity.getDeviceName());
|
||||
}
|
||||
|
||||
public static String buildUsername(String productKey, String deviceName) {
|
||||
return String.format("%s&%s", deviceName, productKey);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
<!-- TODO @芋艿:消息队列,后续可能去掉,默认不使用 rocketmq -->
|
||||
<!-- <optional>true</optional> -->
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
@@ -48,6 +48,13 @@
|
||||
<artifactId>vertx-mqtt</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Modbus 相关 -->
|
||||
<dependency>
|
||||
<groupId>com.ghgande</groupId>
|
||||
<artifactId>j2mod</artifactId>
|
||||
<version>3.2.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- CoAP 相关 - Eclipse Californium -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.californium</groupId>
|
||||
|
||||
@@ -6,6 +6,11 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* IoT 网关配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(IotGatewayProperties.class)
|
||||
public class IotGatewayConfiguration {
|
||||
|
||||
@@ -4,6 +4,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
|
||||
@@ -166,6 +168,18 @@ public class IotGatewayProperties {
|
||||
@Valid
|
||||
private IotEmqxConfig emqx;
|
||||
|
||||
/**
|
||||
* Modbus TCP Client 协议配置
|
||||
*/
|
||||
@Valid
|
||||
private IotModbusTcpClientConfig modbusTcpClient;
|
||||
|
||||
/**
|
||||
* Modbus TCP Server 协议配置
|
||||
*/
|
||||
@Valid
|
||||
private IotModbusTcpServerConfig modbusTcpServer;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public abstract class IotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
public abstract class AbstractIotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
|
||||
private final IotProtocol protocol;
|
||||
|
||||
@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
|
||||
@@ -112,6 +114,10 @@ public class IotProtocolManager implements SmartLifecycle {
|
||||
return createMqttProtocol(config);
|
||||
case EMQX:
|
||||
return createEmqxProtocol(config);
|
||||
case MODBUS_TCP_CLIENT:
|
||||
return createModbusTcpClientProtocol(config);
|
||||
case MODBUS_TCP_SERVER:
|
||||
return createModbusTcpServerProtocol(config);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
|
||||
@@ -188,4 +194,24 @@ public class IotProtocolManager implements SmartLifecycle {
|
||||
return new IotEmqxProtocol(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Modbus TCP Client 协议实例
|
||||
*
|
||||
* @param config 协议实例配置
|
||||
* @return Modbus TCP Client 协议实例
|
||||
*/
|
||||
private IotModbusTcpClientProtocol createModbusTcpClientProtocol(IotGatewayProperties.ProtocolProperties config) {
|
||||
return new IotModbusTcpClientProtocol(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Modbus TCP Server 协议实例
|
||||
*
|
||||
* @param config 协议实例配置
|
||||
* @return Modbus TCP Server 协议实例
|
||||
*/
|
||||
private IotModbusTcpServerProtocol createModbusTcpServerProtocol(IotGatewayProperties.ProtocolProperties config) {
|
||||
return new IotModbusTcpServerProtocol(config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -64,17 +64,13 @@ public class IotCoapProtocol implements IotProtocol {
|
||||
/**
|
||||
* 下行消息订阅者
|
||||
*/
|
||||
private final IotCoapDownstreamSubscriber downstreamSubscriber;
|
||||
private IotCoapDownstreamSubscriber downstreamSubscriber;
|
||||
|
||||
public IotCoapProtocol(ProtocolProperties properties) {
|
||||
IotCoapConfig coapConfig = properties.getCoap();
|
||||
Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空");
|
||||
this.properties = properties;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
|
||||
|
||||
// 初始化下行消息订阅者
|
||||
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
|
||||
this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,9 +90,9 @@ public class IotCoapProtocol implements IotProtocol {
|
||||
return;
|
||||
}
|
||||
|
||||
IotCoapConfig coapConfig = properties.getCoap();
|
||||
try {
|
||||
// 1.1 创建 CoAP 配置
|
||||
IotCoapConfig coapConfig = properties.getCoap();
|
||||
Configuration config = Configuration.createStandardWithoutFile();
|
||||
config.set(CoapConfig.COAP_PORT, properties.getPort());
|
||||
config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize());
|
||||
@@ -131,13 +127,12 @@ public class IotCoapProtocol implements IotProtocol {
|
||||
getId(), properties.getPort(), serverId);
|
||||
|
||||
// 4. 启动下行消息订阅者
|
||||
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
|
||||
this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus);
|
||||
this.downstreamSubscriber.start();
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e);
|
||||
if (coapServer != null) {
|
||||
coapServer.destroy();
|
||||
coapServer = null;
|
||||
}
|
||||
stop0();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -147,12 +142,19 @@ public class IotCoapProtocol implements IotProtocol {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
stop0();
|
||||
}
|
||||
|
||||
private void stop0() {
|
||||
// 1. 停止下行消息订阅者
|
||||
try {
|
||||
downstreamSubscriber.stop();
|
||||
log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e);
|
||||
if (downstreamSubscriber != null) {
|
||||
try {
|
||||
downstreamSubscriber.stop();
|
||||
log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e);
|
||||
}
|
||||
downstreamSubscriber = null;
|
||||
}
|
||||
|
||||
// 2. 关闭 CoAP 服务器
|
||||
|
||||
@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
|
||||
public class IotCoapDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
|
||||
|
||||
public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) {
|
||||
super(protocol, messageBus);
|
||||
|
||||
@@ -90,7 +90,7 @@ public class IotEmqxProtocol implements IotProtocol {
|
||||
/**
|
||||
* 下行消息订阅者
|
||||
*/
|
||||
private final IotEmqxDownstreamSubscriber downstreamSubscriber;
|
||||
private IotEmqxDownstreamSubscriber downstreamSubscriber;
|
||||
|
||||
public IotEmqxProtocol(ProtocolProperties properties) {
|
||||
Assert.notNull(properties, "协议实例配置不能为空");
|
||||
@@ -101,10 +101,6 @@ public class IotEmqxProtocol implements IotProtocol {
|
||||
"MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空");
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
|
||||
this.upstreamHandler = new IotEmqxUpstreamHandler(serverId);
|
||||
|
||||
// 初始化下行消息订阅者
|
||||
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
|
||||
this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -124,7 +120,7 @@ public class IotEmqxProtocol implements IotProtocol {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.1 创建 Vertx 实例
|
||||
// 1.1 创建 Vertx 实例 和 下行消息订阅者
|
||||
this.vertx = Vertx.vertx();
|
||||
|
||||
try {
|
||||
@@ -138,6 +134,8 @@ public class IotEmqxProtocol implements IotProtocol {
|
||||
getId(), properties.getPort(), serverId);
|
||||
|
||||
// 2. 启动下行消息订阅者
|
||||
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
|
||||
this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus);
|
||||
this.downstreamSubscriber.start();
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e);
|
||||
@@ -157,11 +155,14 @@ public class IotEmqxProtocol implements IotProtocol {
|
||||
|
||||
private void stop0() {
|
||||
// 1. 停止下行消息订阅者
|
||||
try {
|
||||
downstreamSubscriber.stop();
|
||||
log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e);
|
||||
if (downstreamSubscriber != null) {
|
||||
try {
|
||||
downstreamSubscriber.stop();
|
||||
log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e);
|
||||
}
|
||||
downstreamSubscriber = null;
|
||||
}
|
||||
|
||||
// 2.1 先置为 false:避免 closeHandler 触发重连
|
||||
|
||||
@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
|
||||
public class IotEmqxDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
|
||||
|
||||
private final IotEmqxDownstreamHandler downstreamHandler;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
import io.vertx.mqtt.messages.MqttPublishMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -42,12 +43,14 @@ public class IotEmqxUpstreamHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 反序列化消息
|
||||
// 2.1 反序列化消息
|
||||
IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
|
||||
if (message == null) {
|
||||
log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
|
||||
return;
|
||||
}
|
||||
// 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀)
|
||||
IotMqttTopicUtils.normalizeReplyMethod(message);
|
||||
|
||||
// 3. 发送消息到队列
|
||||
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
|
||||
|
||||
@@ -64,10 +64,6 @@ public class IotHttpProtocol implements IotProtocol {
|
||||
public IotHttpProtocol(ProtocolProperties properties) {
|
||||
this.properties = properties;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
|
||||
|
||||
// 初始化下行消息订阅者
|
||||
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
|
||||
this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,7 +83,7 @@ public class IotHttpProtocol implements IotProtocol {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.1 创建 Vertx 实例(每个 Protocol 独立管理)
|
||||
// 1.1 创建 Vertx 实例
|
||||
this.vertx = Vertx.vertx();
|
||||
|
||||
// 1.2 创建路由
|
||||
@@ -123,18 +119,12 @@ public class IotHttpProtocol implements IotProtocol {
|
||||
getId(), properties.getPort(), serverId);
|
||||
|
||||
// 2. 启动下行消息订阅者
|
||||
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
|
||||
this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus);
|
||||
this.downstreamSubscriber.start();
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e);
|
||||
// 启动失败时关闭资源
|
||||
if (httpServer != null) {
|
||||
httpServer.close();
|
||||
httpServer = null;
|
||||
}
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
vertx = null;
|
||||
}
|
||||
stop0();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -144,6 +134,10 @@ public class IotHttpProtocol implements IotProtocol {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
stop0();
|
||||
}
|
||||
|
||||
private void stop0() {
|
||||
// 1. 停止下行消息订阅者
|
||||
if (downstreamSubscriber != null) {
|
||||
try {
|
||||
|
||||
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
@@ -13,7 +13,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
*/
|
||||
|
||||
@Slf4j
|
||||
public class IotHttpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
|
||||
public class IotHttpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
|
||||
|
||||
public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) {
|
||||
super(protocol, messageBus);
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
|
||||
import io.vertx.core.Vertx;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
|
||||
|
||||
/**
|
||||
* Modbus 轮询调度器基类
|
||||
* <p>
|
||||
* 封装通用的定时器管理、per-device 请求队列限速逻辑。
|
||||
* 子类只需实现 {@link #pollPoint(Long, Long)} 定义具体的轮询动作。
|
||||
* <p>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractIotModbusPollScheduler {
|
||||
|
||||
protected final Vertx vertx;
|
||||
|
||||
/**
|
||||
* 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积
|
||||
*/
|
||||
private static final long MIN_REQUEST_INTERVAL = 1000;
|
||||
/**
|
||||
* 每个设备请求队列的最大长度,超出时丢弃最旧请求
|
||||
*/
|
||||
private static final int MAX_QUEUE_SIZE = 1000;
|
||||
|
||||
/**
|
||||
* 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo)
|
||||
*/
|
||||
private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* per-device 请求队列:deviceId -> 待执行请求队列
|
||||
*/
|
||||
private final Map<Long, Queue<Runnable>> deviceRequestQueues = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* per-device 上次请求时间戳:deviceId -> lastRequestTimeMs
|
||||
*/
|
||||
private final Map<Long, Long> deviceLastRequestTime = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* per-device 延迟 timer 标记:deviceId -> 是否有延迟 timer 在等待
|
||||
*/
|
||||
private final Map<Long, Boolean> deviceDelayTimerActive = new ConcurrentHashMap<>();
|
||||
|
||||
protected AbstractIotModbusPollScheduler(Vertx vertx) {
|
||||
this.vertx = vertx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点位定时器信息
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
private static class PointTimerInfo {
|
||||
|
||||
/**
|
||||
* Vert.x 定时器 ID
|
||||
*/
|
||||
private Long timerId;
|
||||
/**
|
||||
* 轮询间隔(用于判断是否需要更新定时器)
|
||||
*/
|
||||
private Integer pollInterval;
|
||||
|
||||
}
|
||||
|
||||
// ========== 轮询管理 ==========
|
||||
|
||||
/**
|
||||
* 更新轮询任务(增量更新)
|
||||
*
|
||||
* 1. 【删除】点位:停止对应的轮询定时器
|
||||
* 2. 【新增】点位:创建对应的轮询定时器
|
||||
* 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器
|
||||
* 【修改】其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point)
|
||||
*/
|
||||
public void updatePolling(IotModbusDeviceConfigRespDTO config) {
|
||||
Long deviceId = config.getDeviceId();
|
||||
List<IotModbusPointRespDTO> newPoints = config.getPoints();
|
||||
Map<Long, PointTimerInfo> currentTimers = devicePointTimers
|
||||
.computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
|
||||
// 1.1 计算新配置中的点位 ID 集合
|
||||
Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId);
|
||||
// 1.2 计算删除的点位 ID 集合
|
||||
Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet());
|
||||
removedPointIds.removeAll(newPointIds);
|
||||
|
||||
// 2. 处理删除的点位:停止不再存在的定时器
|
||||
for (Long pointId : removedPointIds) {
|
||||
PointTimerInfo timerInfo = currentTimers.remove(pointId);
|
||||
if (timerInfo != null) {
|
||||
vertx.cancelTimer(timerInfo.getTimerId());
|
||||
log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 处理新增和修改的点位
|
||||
if (CollUtil.isEmpty(newPoints)) {
|
||||
return;
|
||||
}
|
||||
for (IotModbusPointRespDTO point : newPoints) {
|
||||
Long pointId = point.getId();
|
||||
Integer newPollInterval = point.getPollInterval();
|
||||
PointTimerInfo existingTimer = currentTimers.get(pointId);
|
||||
// 3.1 新增点位:创建定时器
|
||||
if (existingTimer == null) {
|
||||
Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
|
||||
if (timerId != null) {
|
||||
currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
|
||||
log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
|
||||
deviceId, pointId, newPollInterval);
|
||||
}
|
||||
} else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
|
||||
// 3.2 pollInterval 变化:重建定时器
|
||||
vertx.cancelTimer(existingTimer.getTimerId());
|
||||
Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
|
||||
if (timerId != null) {
|
||||
currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
|
||||
log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
|
||||
deviceId, pointId, existingTimer.getPollInterval(), newPollInterval);
|
||||
} else {
|
||||
currentTimers.remove(pointId);
|
||||
}
|
||||
}
|
||||
// 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建轮询定时器
|
||||
*/
|
||||
private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) {
|
||||
if (pollInterval == null || pollInterval <= 0) {
|
||||
return null;
|
||||
}
|
||||
return vertx.setPeriodic(pollInterval, timerId -> {
|
||||
try {
|
||||
submitPollRequest(deviceId, pointId);
|
||||
} catch (Exception e) {
|
||||
log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 请求队列(per-device 限速) ==========
|
||||
|
||||
/**
|
||||
* 提交轮询请求到设备请求队列(保证同设备请求间隔)
|
||||
*/
|
||||
private void submitPollRequest(Long deviceId, Long pointId) {
|
||||
// 1. 【重要】将请求添加到设备的请求队列
|
||||
Queue<Runnable> queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>());
|
||||
while (queue.size() >= MAX_QUEUE_SIZE) {
|
||||
// 超出上限时,丢弃最旧的请求
|
||||
queue.poll();
|
||||
log.warn("[submitPollRequest][设备 {} 请求队列已满({}), 丢弃最旧请求]", deviceId, MAX_QUEUE_SIZE);
|
||||
}
|
||||
queue.offer(() -> pollPoint(deviceId, pointId));
|
||||
|
||||
// 2. 处理设备请求队列(如果没有延迟 timer 在等待)
|
||||
processDeviceQueue(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备请求队列
|
||||
*/
|
||||
private void processDeviceQueue(Long deviceId) {
|
||||
Queue<Runnable> queue = deviceRequestQueues.get(deviceId);
|
||||
if (CollUtil.isEmpty(queue)) {
|
||||
return;
|
||||
}
|
||||
// 检查是否已有延迟 timer 在等待
|
||||
if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 不满足间隔要求,延迟执行
|
||||
long now = System.currentTimeMillis();
|
||||
long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L);
|
||||
long elapsed = now - lastTime;
|
||||
if (elapsed < MIN_REQUEST_INTERVAL) {
|
||||
scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed);
|
||||
return;
|
||||
}
|
||||
|
||||
// 满足间隔要求,立即执行
|
||||
Runnable task = queue.poll();
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
deviceLastRequestTime.put(deviceId, now);
|
||||
task.run();
|
||||
// 继续处理队列中的下一个(如果有的话,需要延迟)
|
||||
if (CollUtil.isNotEmpty(queue)) {
|
||||
scheduleNextRequest(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleNextRequest(Long deviceId) {
|
||||
scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL);
|
||||
}
|
||||
|
||||
private void scheduleNextRequest(Long deviceId, long delayMs) {
|
||||
deviceDelayTimerActive.put(deviceId, true);
|
||||
vertx.setTimer(delayMs, id -> {
|
||||
deviceDelayTimerActive.put(deviceId, false);
|
||||
Queue<Runnable> queue = deviceRequestQueues.get(deviceId);
|
||||
if (CollUtil.isEmpty(queue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 满足间隔要求,立即执行
|
||||
Runnable task = queue.poll();
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
deviceLastRequestTime.put(deviceId, System.currentTimeMillis());
|
||||
task.run();
|
||||
// 继续处理队列中的下一个(如果有的话,需要延迟)
|
||||
if (CollUtil.isNotEmpty(queue)) {
|
||||
scheduleNextRequest(deviceId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 轮询执行 ==========
|
||||
|
||||
/**
|
||||
* 轮询单个点位(子类实现具体的读取逻辑)
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @param pointId 点位 ID
|
||||
*/
|
||||
protected abstract void pollPoint(Long deviceId, Long pointId);
|
||||
|
||||
// ========== 停止 ==========
|
||||
|
||||
/**
|
||||
* 停止设备的轮询
|
||||
*/
|
||||
public void stopPolling(Long deviceId) {
|
||||
Map<Long, PointTimerInfo> timers = devicePointTimers.remove(deviceId);
|
||||
if (CollUtil.isEmpty(timers)) {
|
||||
return;
|
||||
}
|
||||
for (PointTimerInfo timerInfo : timers.values()) {
|
||||
vertx.cancelTimer(timerInfo.getTimerId());
|
||||
}
|
||||
// 清理请求队列
|
||||
deviceRequestQueues.remove(deviceId);
|
||||
deviceLastRequestTime.remove(deviceId);
|
||||
deviceDelayTimerActive.remove(deviceId);
|
||||
log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止所有轮询
|
||||
*/
|
||||
public void stopAll() {
|
||||
for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) {
|
||||
stopPolling(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* IoT Modbus 协议工具类
|
||||
* <p>
|
||||
* 提供 Modbus 协议全链路能力:
|
||||
* <ul>
|
||||
* <li>协议常量:功能码(FC01~FC16)、异常掩码等</li>
|
||||
* <li>功能码判断:读/写/异常分类、可写判断、写功能码映射</li>
|
||||
* <li>CRC-16/MODBUS 计算和校验</li>
|
||||
* <li>数据转换:原始值 ↔ 物模型属性值({@link #convertToPropertyValue} / {@link #convertToRawValues})</li>
|
||||
* <li>帧值提取:从 Modbus 帧提取寄存器/线圈值({@link #extractValues})</li>
|
||||
* <li>点位查找({@link #findPoint})</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@UtilityClass
|
||||
@Slf4j
|
||||
public class IotModbusCommonUtils {
|
||||
|
||||
/** FC01: 读线圈 */
|
||||
public static final int FC_READ_COILS = 1;
|
||||
/** FC02: 读离散输入 */
|
||||
public static final int FC_READ_DISCRETE_INPUTS = 2;
|
||||
/** FC03: 读保持寄存器 */
|
||||
public static final int FC_READ_HOLDING_REGISTERS = 3;
|
||||
/** FC04: 读输入寄存器 */
|
||||
public static final int FC_READ_INPUT_REGISTERS = 4;
|
||||
|
||||
/** FC05: 写单个线圈 */
|
||||
public static final int FC_WRITE_SINGLE_COIL = 5;
|
||||
/** FC06: 写单个寄存器 */
|
||||
public static final int FC_WRITE_SINGLE_REGISTER = 6;
|
||||
/** FC15: 写多个线圈 */
|
||||
public static final int FC_WRITE_MULTIPLE_COILS = 15;
|
||||
/** FC16: 写多个寄存器 */
|
||||
public static final int FC_WRITE_MULTIPLE_REGISTERS = 16;
|
||||
|
||||
/**
|
||||
* 异常响应掩码:响应帧的功能码最高位为 1 时,表示异常响应
|
||||
* 例如:请求 FC=0x03,异常响应 FC=0x83(0x03 | 0x80)
|
||||
*/
|
||||
public static final int FC_EXCEPTION_MASK = 0x80;
|
||||
|
||||
/**
|
||||
* 功能码掩码:用于从异常响应中提取原始功能码
|
||||
* 例如:异常 FC=0x83,原始 FC = 0x83 & 0x7F = 0x03
|
||||
*/
|
||||
public static final int FC_MASK = 0x7F;
|
||||
|
||||
// ==================== 功能码分类判断 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为读响应(FC01-04)
|
||||
*/
|
||||
public static boolean isReadResponse(int functionCode) {
|
||||
return functionCode >= FC_READ_COILS && functionCode <= FC_READ_INPUT_REGISTERS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为写响应(FC05/06/15/16)
|
||||
*/
|
||||
public static boolean isWriteResponse(int functionCode) {
|
||||
return functionCode == FC_WRITE_SINGLE_COIL || functionCode == FC_WRITE_SINGLE_REGISTER
|
||||
|| functionCode == FC_WRITE_MULTIPLE_COILS || functionCode == FC_WRITE_MULTIPLE_REGISTERS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为异常响应
|
||||
*/
|
||||
public static boolean isExceptionResponse(int functionCode) {
|
||||
return (functionCode & FC_EXCEPTION_MASK) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从异常响应中提取原始功能码
|
||||
*/
|
||||
public static int extractOriginalFunctionCode(int exceptionFunctionCode) {
|
||||
return exceptionFunctionCode & FC_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断读功能码是否支持写操作
|
||||
* <p>
|
||||
* FC01(读线圈)和 FC03(读保持寄存器)支持写操作;
|
||||
* FC02(读离散输入)和 FC04(读输入寄存器)为只读。
|
||||
*
|
||||
* @param readFunctionCode 读功能码(FC01-04)
|
||||
* @return 是否支持写操作
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public static boolean isWritable(int readFunctionCode) {
|
||||
return readFunctionCode == FC_READ_COILS || readFunctionCode == FC_READ_HOLDING_REGISTERS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单写功能码
|
||||
* <p>
|
||||
* FC01(读线圈)→ FC05(写单个线圈);
|
||||
* FC03(读保持寄存器)→ FC06(写单个寄存器);
|
||||
* 其他返回 null(不支持写)。
|
||||
*
|
||||
* @param readFunctionCode 读功能码
|
||||
* @return 单写功能码,不支持写时返回 null
|
||||
*/
|
||||
@SuppressWarnings("EnhancedSwitchMigration")
|
||||
public static Integer getWriteSingleFunctionCode(int readFunctionCode) {
|
||||
switch (readFunctionCode) {
|
||||
case FC_READ_COILS:
|
||||
return FC_WRITE_SINGLE_COIL;
|
||||
case FC_READ_HOLDING_REGISTERS:
|
||||
return FC_WRITE_SINGLE_REGISTER;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多写功能码
|
||||
* <p>
|
||||
* FC01(读线圈)→ FC15(写多个线圈);
|
||||
* FC03(读保持寄存器)→ FC16(写多个寄存器);
|
||||
* 其他返回 null(不支持写)。
|
||||
*
|
||||
* @param readFunctionCode 读功能码
|
||||
* @return 多写功能码,不支持写时返回 null
|
||||
*/
|
||||
@SuppressWarnings("EnhancedSwitchMigration")
|
||||
public static Integer getWriteMultipleFunctionCode(int readFunctionCode) {
|
||||
switch (readFunctionCode) {
|
||||
case FC_READ_COILS:
|
||||
return FC_WRITE_MULTIPLE_COILS;
|
||||
case FC_READ_HOLDING_REGISTERS:
|
||||
return FC_WRITE_MULTIPLE_REGISTERS;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CRC16 工具 ====================
|
||||
|
||||
/**
|
||||
* 计算 CRC-16/MODBUS
|
||||
*
|
||||
* @param data 数据
|
||||
* @param length 计算长度
|
||||
* @return CRC16 值
|
||||
*/
|
||||
public static int calculateCrc16(byte[] data, int length) {
|
||||
int crc = 0xFFFF;
|
||||
for (int i = 0; i < length; i++) {
|
||||
crc ^= (data[i] & 0xFF);
|
||||
for (int j = 0; j < 8; j++) {
|
||||
if ((crc & 0x0001) != 0) {
|
||||
crc >>= 1;
|
||||
crc ^= 0xA001;
|
||||
} else {
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 CRC16
|
||||
*
|
||||
* @param data 包含 CRC 的完整数据
|
||||
* @return 校验是否通过
|
||||
*/
|
||||
public static boolean verifyCrc16(byte[] data) {
|
||||
if (data.length < 3) {
|
||||
return false;
|
||||
}
|
||||
int computed = calculateCrc16(data, data.length - 2);
|
||||
int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8);
|
||||
return computed == received;
|
||||
}
|
||||
|
||||
// ==================== 数据转换 ====================
|
||||
|
||||
/**
|
||||
* 将原始值转换为物模型属性值
|
||||
*
|
||||
* @param rawValues 原始值数组(寄存器值或线圈值)
|
||||
* @param point 点位配置
|
||||
* @return 转换后的属性值
|
||||
*/
|
||||
public static Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) {
|
||||
if (ArrayUtil.isEmpty(rawValues)) {
|
||||
return null;
|
||||
}
|
||||
String rawDataType = point.getRawDataType();
|
||||
String byteOrder = point.getByteOrder();
|
||||
BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE);
|
||||
|
||||
// 1. 根据原始数据类型解析原始数值
|
||||
Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder);
|
||||
if (rawNumber == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 应用缩放因子:实际值 = 原始值 × scale
|
||||
BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale);
|
||||
|
||||
// 3. 根据数据类型返回合适的 Java 类型
|
||||
return formatValue(actualValue, rawDataType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将物模型属性值转换为原始寄存器值
|
||||
*
|
||||
* @param propertyValue 属性值
|
||||
* @param point 点位配置
|
||||
* @return 原始值数组
|
||||
*/
|
||||
public static int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) {
|
||||
if (propertyValue == null) {
|
||||
return new int[0];
|
||||
}
|
||||
String rawDataType = point.getRawDataType();
|
||||
String byteOrder = point.getByteOrder();
|
||||
BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE);
|
||||
int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1);
|
||||
|
||||
// 1. 转换为 BigDecimal
|
||||
BigDecimal actualValue = new BigDecimal(propertyValue.toString());
|
||||
|
||||
// 2. 应用缩放因子:原始值 = 实际值 ÷ scale
|
||||
BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP);
|
||||
|
||||
// 3. 根据原始数据类型编码为寄存器值
|
||||
return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount);
|
||||
}
|
||||
|
||||
@SuppressWarnings("EnhancedSwitchMigration")
|
||||
private static Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) {
|
||||
IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
|
||||
if (dataTypeEnum == null) {
|
||||
log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType);
|
||||
return rawValues[0];
|
||||
}
|
||||
switch (dataTypeEnum) {
|
||||
case BOOLEAN:
|
||||
return rawValues[0] != 0 ? 1 : 0;
|
||||
case INT16:
|
||||
return (short) rawValues[0];
|
||||
case UINT16:
|
||||
return rawValues[0] & 0xFFFF;
|
||||
case INT32:
|
||||
return parseInt32(rawValues, byteOrder);
|
||||
case UINT32:
|
||||
return parseUint32(rawValues, byteOrder);
|
||||
case FLOAT:
|
||||
return parseFloat(rawValues, byteOrder);
|
||||
case DOUBLE:
|
||||
return parseDouble(rawValues, byteOrder);
|
||||
default:
|
||||
log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType);
|
||||
return rawValues[0];
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseInt32(int[] rawValues, String byteOrder) {
|
||||
if (rawValues.length < 2) {
|
||||
return rawValues[0];
|
||||
}
|
||||
byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
|
||||
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt();
|
||||
}
|
||||
|
||||
private static long parseUint32(int[] rawValues, String byteOrder) {
|
||||
if (rawValues.length < 2) {
|
||||
return rawValues[0] & 0xFFFFFFFFL;
|
||||
}
|
||||
byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
|
||||
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL;
|
||||
}
|
||||
|
||||
private static float parseFloat(int[] rawValues, String byteOrder) {
|
||||
if (rawValues.length < 2) {
|
||||
return (float) rawValues[0];
|
||||
}
|
||||
byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
|
||||
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat();
|
||||
}
|
||||
|
||||
private static double parseDouble(int[] rawValues, String byteOrder) {
|
||||
if (rawValues.length < 4) {
|
||||
return rawValues[0];
|
||||
}
|
||||
byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder);
|
||||
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble();
|
||||
}
|
||||
|
||||
private static byte[] registersToBytes(int[] registers, int count) {
|
||||
byte[] bytes = new byte[count * 2];
|
||||
for (int i = 0; i < Math.min(registers.length, count); i++) {
|
||||
bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF);
|
||||
bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@SuppressWarnings("EnhancedSwitchMigration")
|
||||
private static byte[] reorderBytes(byte[] bytes, String byteOrder) {
|
||||
IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder);
|
||||
// null 或者大端序,不需要调整
|
||||
if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// 其他字节序调整
|
||||
byte[] result = new byte[bytes.length];
|
||||
switch (byteOrderEnum) {
|
||||
case BA: // 小端序:按每 2 字节一组交换(16 位场景 [1,0],32 位场景 [1,0,3,2])
|
||||
for (int i = 0; i + 1 < bytes.length; i += 2) {
|
||||
result[i] = bytes[i + 1];
|
||||
result[i + 1] = bytes[i];
|
||||
}
|
||||
break;
|
||||
case CDAB: // 大端字交换(32 位)
|
||||
if (bytes.length >= 4) {
|
||||
result[0] = bytes[2];
|
||||
result[1] = bytes[3];
|
||||
result[2] = bytes[0];
|
||||
result[3] = bytes[1];
|
||||
}
|
||||
break;
|
||||
case DCBA: // 小端序(32 位)
|
||||
if (bytes.length >= 4) {
|
||||
result[0] = bytes[3];
|
||||
result[1] = bytes[2];
|
||||
result[2] = bytes[1];
|
||||
result[3] = bytes[0];
|
||||
}
|
||||
break;
|
||||
case BADC: // 小端字交换(32 位)
|
||||
if (bytes.length >= 4) {
|
||||
result[0] = bytes[1];
|
||||
result[1] = bytes[0];
|
||||
result[2] = bytes[3];
|
||||
result[3] = bytes[2];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return bytes;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("EnhancedSwitchMigration")
|
||||
private static int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) {
|
||||
IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
|
||||
if (dataTypeEnum == null) {
|
||||
return new int[]{rawValue.intValue()};
|
||||
}
|
||||
switch (dataTypeEnum) {
|
||||
case BOOLEAN:
|
||||
return new int[]{rawValue.intValue() != 0 ? 1 : 0};
|
||||
case INT16:
|
||||
case UINT16:
|
||||
return new int[]{rawValue.intValue() & 0xFFFF};
|
||||
case INT32:
|
||||
return encodeInt32(rawValue.intValue(), byteOrder);
|
||||
case UINT32:
|
||||
// 使用 longValue() 避免超过 Integer.MAX_VALUE 时溢出,
|
||||
// 强转 int 保留低 32 位 bit pattern,写入寄存器的字节是正确的无符号值
|
||||
return encodeInt32((int) rawValue.longValue(), byteOrder);
|
||||
case FLOAT:
|
||||
return encodeFloat(rawValue.floatValue(), byteOrder);
|
||||
case DOUBLE:
|
||||
return encodeDouble(rawValue.doubleValue(), byteOrder);
|
||||
default:
|
||||
return new int[]{rawValue.intValue()};
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] encodeInt32(int value, String byteOrder) {
|
||||
byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array();
|
||||
bytes = reorderBytes(bytes, byteOrder);
|
||||
return bytesToRegisters(bytes);
|
||||
}
|
||||
|
||||
private static int[] encodeFloat(float value, String byteOrder) {
|
||||
byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array();
|
||||
bytes = reorderBytes(bytes, byteOrder);
|
||||
return bytesToRegisters(bytes);
|
||||
}
|
||||
|
||||
private static int[] encodeDouble(double value, String byteOrder) {
|
||||
byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array();
|
||||
bytes = reorderBytes(bytes, byteOrder);
|
||||
return bytesToRegisters(bytes);
|
||||
}
|
||||
|
||||
private static int[] bytesToRegisters(byte[] bytes) {
|
||||
int[] registers = new int[bytes.length / 2];
|
||||
for (int i = 0; i < registers.length; i++) {
|
||||
registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF);
|
||||
}
|
||||
return registers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("EnhancedSwitchMigration")
|
||||
private static Object formatValue(BigDecimal value, String rawDataType) {
|
||||
IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
|
||||
if (dataTypeEnum == null) {
|
||||
return value;
|
||||
}
|
||||
switch (dataTypeEnum) {
|
||||
case BOOLEAN:
|
||||
return value.intValue() != 0;
|
||||
case INT16:
|
||||
case INT32:
|
||||
return value.intValue();
|
||||
case UINT16:
|
||||
case UINT32:
|
||||
return value.longValue();
|
||||
case FLOAT:
|
||||
return value.floatValue();
|
||||
case DOUBLE:
|
||||
return value.doubleValue();
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 帧值提取 ====================
|
||||
|
||||
/**
|
||||
* 从帧中提取寄存器值(FC01-04 读响应)
|
||||
*
|
||||
* @param frame 解码后的 Modbus 帧
|
||||
* @return 寄存器值数组(int[]),失败返回 null
|
||||
*/
|
||||
@SuppressWarnings("EnhancedSwitchMigration")
|
||||
public static int[] extractValues(IotModbusFrame frame) {
|
||||
if (frame == null || frame.isException()) {
|
||||
return null;
|
||||
}
|
||||
byte[] pdu = frame.getPdu();
|
||||
if (pdu == null || pdu.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int functionCode = frame.getFunctionCode();
|
||||
switch (functionCode) {
|
||||
case FC_READ_COILS:
|
||||
case FC_READ_DISCRETE_INPUTS:
|
||||
return extractCoilValues(pdu);
|
||||
case FC_READ_HOLDING_REGISTERS:
|
||||
case FC_READ_INPUT_REGISTERS:
|
||||
return extractRegisterValues(pdu);
|
||||
default:
|
||||
log.warn("[extractValues][不支持的功能码: {}]", functionCode);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] extractCoilValues(byte[] pdu) {
|
||||
if (pdu.length < 2) {
|
||||
return null;
|
||||
}
|
||||
int byteCount = pdu[0] & 0xFF;
|
||||
int bitCount = byteCount * 8;
|
||||
int[] values = new int[bitCount];
|
||||
for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) {
|
||||
values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static int[] extractRegisterValues(byte[] pdu) {
|
||||
if (pdu.length < 2) {
|
||||
return null;
|
||||
}
|
||||
int byteCount = pdu[0] & 0xFF;
|
||||
int registerCount = byteCount / 2;
|
||||
int[] values = new int[registerCount];
|
||||
for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) {
|
||||
values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应帧中提取 registerCount(通过 PDU 的 byteCount 推断)
|
||||
*
|
||||
* @param frame 解码后的 Modbus 响应帧
|
||||
* @return registerCount,无法提取时返回 -1(匹配时跳过校验)
|
||||
*/
|
||||
public static int extractRegisterCountFromResponse(IotModbusFrame frame) {
|
||||
byte[] pdu = frame.getPdu();
|
||||
if (pdu == null || pdu.length < 1) {
|
||||
return -1;
|
||||
}
|
||||
int byteCount = pdu[0] & 0xFF;
|
||||
int fc = frame.getFunctionCode();
|
||||
// FC03/04 寄存器读响应:registerCount = byteCount / 2
|
||||
if (fc == FC_READ_HOLDING_REGISTERS || fc == FC_READ_INPUT_REGISTERS) {
|
||||
return byteCount / 2;
|
||||
}
|
||||
// FC01/02 线圈/离散输入读响应:按 bit 打包有余位,无法精确反推,返回 -1 跳过校验
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ==================== 点位查找 ====================
|
||||
|
||||
/**
|
||||
* 查找点位配置
|
||||
*
|
||||
* @param config 设备 Modbus 配置
|
||||
* @param identifier 点位标识符
|
||||
* @return 匹配的点位配置,未找到返回 null
|
||||
*/
|
||||
public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) {
|
||||
if (config == null || StrUtil.isBlank(identifier)) {
|
||||
return null;
|
||||
}
|
||||
return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据点位 ID 查找点位配置
|
||||
*
|
||||
* @param config 设备 Modbus 配置
|
||||
* @param pointId 点位 ID
|
||||
* @return 匹配的点位配置,未找到返回 null
|
||||
*/
|
||||
public static IotModbusPointRespDTO findPointById(IotModbusDeviceConfigRespDTO config, Long pointId) {
|
||||
if (config == null || pointId == null) {
|
||||
return null;
|
||||
}
|
||||
return CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
|
||||
import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction;
|
||||
import com.ghgande.j2mod.modbus.msg.*;
|
||||
import com.ghgande.j2mod.modbus.procimg.InputRegister;
|
||||
import com.ghgande.j2mod.modbus.procimg.Register;
|
||||
import com.ghgande.j2mod.modbus.procimg.SimpleRegister;
|
||||
import com.ghgande.j2mod.modbus.util.BitVector;
|
||||
import io.vertx.core.Future;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.*;
|
||||
|
||||
/**
|
||||
* IoT Modbus TCP 客户端工具类
|
||||
* <p>
|
||||
* 封装基于 j2mod 的 Modbus TCP 读写操作:
|
||||
* 1. 根据功能码创建对应的 Modbus 读/写请求
|
||||
* 2. 通过 {@link IotModbusTcpClientConnectionManager.ModbusConnection} 执行事务
|
||||
* 3. 从响应中提取原始值
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@UtilityClass
|
||||
@Slf4j
|
||||
public class IotModbusTcpClientUtils {
|
||||
|
||||
/**
|
||||
* 读取 Modbus 数据
|
||||
*
|
||||
* @param connection Modbus 连接
|
||||
* @param slaveId 从站地址
|
||||
* @param point 点位配置
|
||||
* @return 原始值(int 数组)
|
||||
*/
|
||||
public static Future<int[]> read(IotModbusTcpClientConnectionManager.ModbusConnection connection,
|
||||
Integer slaveId,
|
||||
IotModbusPointRespDTO point) {
|
||||
return connection.executeBlocking(tcpConnection -> {
|
||||
try {
|
||||
// 1. 创建请求
|
||||
ModbusRequest request = createReadRequest(point.getFunctionCode(),
|
||||
point.getRegisterAddress(), point.getRegisterCount());
|
||||
request.setUnitID(slaveId);
|
||||
|
||||
// 2. 执行事务(请求)
|
||||
ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection);
|
||||
transaction.setRequest(request);
|
||||
transaction.execute();
|
||||
|
||||
// 3. 解析响应
|
||||
ModbusResponse response = transaction.getResponse();
|
||||
return extractValues(response, point.getFunctionCode());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(String.format("Modbus 读取失败 [slaveId=%d, identifier=%s, address=%d]",
|
||||
slaveId, point.getIdentifier(), point.getRegisterAddress()), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入 Modbus 数据
|
||||
*
|
||||
* @param connection Modbus 连接
|
||||
* @param slaveId 从站地址
|
||||
* @param point 点位配置
|
||||
* @param values 要写入的值
|
||||
* @return 是否成功
|
||||
*/
|
||||
public static Future<Boolean> write(IotModbusTcpClientConnectionManager.ModbusConnection connection,
|
||||
Integer slaveId,
|
||||
IotModbusPointRespDTO point,
|
||||
int[] values) {
|
||||
return connection.executeBlocking(tcpConnection -> {
|
||||
try {
|
||||
// 1. 创建请求
|
||||
ModbusRequest request = createWriteRequest(point.getFunctionCode(),
|
||||
point.getRegisterAddress(), point.getRegisterCount(), values);
|
||||
if (request == null) {
|
||||
throw new RuntimeException("功能码 " + point.getFunctionCode() + " 不支持写操作");
|
||||
}
|
||||
request.setUnitID(slaveId);
|
||||
|
||||
// 2. 执行事务(请求)
|
||||
ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection);
|
||||
transaction.setRequest(request);
|
||||
transaction.execute();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(String.format("Modbus 写入失败 [slaveId=%d, identifier=%s, address=%d]",
|
||||
slaveId, point.getIdentifier(), point.getRegisterAddress()), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建读取请求
|
||||
*/
|
||||
@SuppressWarnings("EnhancedSwitchMigration")
|
||||
private static ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) {
|
||||
switch (functionCode) {
|
||||
case FC_READ_COILS:
|
||||
return new ReadCoilsRequest(address, count);
|
||||
case FC_READ_DISCRETE_INPUTS:
|
||||
return new ReadInputDiscretesRequest(address, count);
|
||||
case FC_READ_HOLDING_REGISTERS:
|
||||
return new ReadMultipleRegistersRequest(address, count);
|
||||
case FC_READ_INPUT_REGISTERS:
|
||||
return new ReadInputRegistersRequest(address, count);
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的功能码: " + functionCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建写入请求
|
||||
*/
|
||||
@SuppressWarnings("EnhancedSwitchMigration")
|
||||
private static ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) {
|
||||
switch (functionCode) {
|
||||
case FC_READ_COILS: // 写线圈(使用功能码 5 或 15)
|
||||
if (count == 1) {
|
||||
return new WriteCoilRequest(address, values[0] != 0);
|
||||
} else {
|
||||
BitVector bv = new BitVector(count);
|
||||
for (int i = 0; i < Math.min(values.length, count); i++) {
|
||||
bv.setBit(i, values[i] != 0);
|
||||
}
|
||||
return new WriteMultipleCoilsRequest(address, bv);
|
||||
}
|
||||
case FC_READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16)
|
||||
if (count == 1) {
|
||||
return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0]));
|
||||
} else {
|
||||
Register[] registers = new SimpleRegister[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
registers[i] = new SimpleRegister(i < values.length ? values[i] : 0);
|
||||
}
|
||||
return new WriteMultipleRegistersRequest(address, registers);
|
||||
}
|
||||
case FC_READ_DISCRETE_INPUTS: // 只读
|
||||
case FC_READ_INPUT_REGISTERS: // 只读
|
||||
return null;
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的功能码: " + functionCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中提取值
|
||||
*/
|
||||
@SuppressWarnings("EnhancedSwitchMigration")
|
||||
private static int[] extractValues(ModbusResponse response, Integer functionCode) {
|
||||
switch (functionCode) {
|
||||
case FC_READ_COILS:
|
||||
ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response;
|
||||
int bitCount = coilsResponse.getBitCount();
|
||||
int[] coilValues = new int[bitCount];
|
||||
for (int i = 0; i < bitCount; i++) {
|
||||
coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0;
|
||||
}
|
||||
return coilValues;
|
||||
case FC_READ_DISCRETE_INPUTS:
|
||||
ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response;
|
||||
int discreteCount = discretesResponse.getBitCount();
|
||||
int[] discreteValues = new int[discreteCount];
|
||||
for (int i = 0; i < discreteCount; i++) {
|
||||
discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0;
|
||||
}
|
||||
return discreteValues;
|
||||
case FC_READ_HOLDING_REGISTERS:
|
||||
ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response;
|
||||
InputRegister[] holdingRegisters = holdingResponse.getRegisters();
|
||||
int[] holdingValues = new int[holdingRegisters.length];
|
||||
for (int i = 0; i < holdingRegisters.length; i++) {
|
||||
holdingValues[i] = holdingRegisters[i].getValue();
|
||||
}
|
||||
return holdingValues;
|
||||
case FC_READ_INPUT_REGISTERS:
|
||||
ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response;
|
||||
InputRegister[] inputRegisters = inputResponse.getRegisters();
|
||||
int[] inputValues = new int[inputRegisters.length];
|
||||
for (int i = 0; i < inputRegisters.length; i++) {
|
||||
inputValues[i] = inputRegisters[i].getValue();
|
||||
}
|
||||
return inputValues;
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的功能码: " + functionCode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT Modbus TCP Client 协议配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class IotModbusTcpClientConfig {
|
||||
|
||||
/**
|
||||
* 配置刷新间隔(秒)
|
||||
*/
|
||||
@NotNull(message = "配置刷新间隔不能为空")
|
||||
@Min(value = 1, message = "配置刷新间隔不能小于 1 秒")
|
||||
private Integer configRefreshInterval = 30;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientPollScheduler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Vertx;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RedissonClient;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* IoT 网关 Modbus TCP Client 协议:主动轮询 Modbus 从站设备数据
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotModbusTcpClientProtocol implements IotProtocol {
|
||||
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
private final ProtocolProperties properties;
|
||||
/**
|
||||
* 服务器 ID(用于消息追踪,全局唯一)
|
||||
*/
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
/**
|
||||
* 运行状态
|
||||
*/
|
||||
@Getter
|
||||
private volatile boolean running = false;
|
||||
|
||||
/**
|
||||
* Vert.x 实例
|
||||
*/
|
||||
private final Vertx vertx;
|
||||
/**
|
||||
* 配置刷新定时器 ID
|
||||
*/
|
||||
private Long configRefreshTimerId;
|
||||
|
||||
/**
|
||||
* 连接管理器
|
||||
*/
|
||||
private final IotModbusTcpClientConnectionManager connectionManager;
|
||||
/**
|
||||
* 下行消息订阅者
|
||||
*/
|
||||
private IotModbusTcpClientDownstreamSubscriber downstreamSubscriber;
|
||||
|
||||
private final IotModbusTcpClientConfigCacheService configCacheService;
|
||||
private final IotModbusTcpClientPollScheduler pollScheduler;
|
||||
|
||||
public IotModbusTcpClientProtocol(ProtocolProperties properties) {
|
||||
IotModbusTcpClientConfig modbusTcpClientConfig = properties.getModbusTcpClient();
|
||||
Assert.notNull(modbusTcpClientConfig, "Modbus TCP Client 协议配置(modbusTcpClient)不能为空");
|
||||
this.properties = properties;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
|
||||
|
||||
// 初始化 Vertx
|
||||
this.vertx = Vertx.vertx();
|
||||
|
||||
// 初始化 Manager
|
||||
RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class);
|
||||
IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
this.configCacheService = new IotModbusTcpClientConfigCacheService(deviceApi);
|
||||
this.connectionManager = new IotModbusTcpClientConnectionManager(redissonClient, vertx,
|
||||
messageService, configCacheService, serverId);
|
||||
|
||||
// 初始化 Handler
|
||||
IotModbusTcpClientUpstreamHandler upstreamHandler = new IotModbusTcpClientUpstreamHandler(messageService, serverId);
|
||||
|
||||
// 初始化轮询调度器
|
||||
this.pollScheduler = new IotModbusTcpClientPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return properties.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotProtocolTypeEnum getType() {
|
||||
return IotProtocolTypeEnum.MODBUS_TCP_CLIENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (running) {
|
||||
log.warn("[start][IoT Modbus TCP Client 协议 {} 已经在运行中]", getId());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1.1 首次加载配置
|
||||
refreshConfig();
|
||||
// 1.2 启动配置刷新定时器
|
||||
int refreshInterval = properties.getModbusTcpClient().getConfigRefreshInterval();
|
||||
configRefreshTimerId = vertx.setPeriodic(
|
||||
TimeUnit.SECONDS.toMillis(refreshInterval),
|
||||
id -> refreshConfig()
|
||||
);
|
||||
running = true;
|
||||
log.info("[start][IoT Modbus TCP Client 协议 {} 启动成功,serverId={}]", getId(), serverId);
|
||||
|
||||
// 2. 启动下行消息订阅者
|
||||
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
|
||||
IotModbusTcpClientDownstreamHandler downstreamHandler = new IotModbusTcpClientDownstreamHandler(connectionManager,
|
||||
configCacheService);
|
||||
this.downstreamSubscriber = new IotModbusTcpClientDownstreamSubscriber(this, downstreamHandler, messageBus);
|
||||
this.downstreamSubscriber.start();
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT Modbus TCP Client 协议 {} 启动失败]", getId(), e);
|
||||
stop0();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
stop0();
|
||||
}
|
||||
|
||||
private void stop0() {
|
||||
// 1. 停止下行消息订阅者
|
||||
if (downstreamSubscriber != null) {
|
||||
try {
|
||||
downstreamSubscriber.stop();
|
||||
log.info("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者已停止]", getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者停止失败]", getId(), e);
|
||||
}
|
||||
downstreamSubscriber = null;
|
||||
}
|
||||
|
||||
// 2.1 取消配置刷新定时器
|
||||
if (configRefreshTimerId != null) {
|
||||
vertx.cancelTimer(configRefreshTimerId);
|
||||
configRefreshTimerId = null;
|
||||
}
|
||||
// 2.2 停止轮询调度器
|
||||
pollScheduler.stopAll();
|
||||
// 2.3 关闭所有连接
|
||||
connectionManager.closeAll();
|
||||
|
||||
// 3. 关闭 Vert.x 实例
|
||||
if (vertx != null) {
|
||||
try {
|
||||
vertx.close().result();
|
||||
log.info("[stop][IoT Modbus TCP Client 协议 {} Vertx 已关闭]", getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT Modbus TCP Client 协议 {} Vertx 关闭失败]", getId(), e);
|
||||
}
|
||||
}
|
||||
running = false;
|
||||
log.info("[stop][IoT Modbus TCP Client 协议 {} 已停止]", getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新配置
|
||||
*/
|
||||
private synchronized void refreshConfig() {
|
||||
try {
|
||||
// 1. 从 biz 拉取最新配置(API 失败时返回 null)
|
||||
List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig();
|
||||
if (configs == null) {
|
||||
log.warn("[refreshConfig][API 失败,跳过本轮刷新]");
|
||||
return;
|
||||
}
|
||||
log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size());
|
||||
|
||||
// 2. 更新连接和轮询任务
|
||||
for (IotModbusDeviceConfigRespDTO config : configs) {
|
||||
try {
|
||||
// 2.1 确保连接存在
|
||||
connectionManager.ensureConnection(config);
|
||||
// 2.2 更新轮询任务
|
||||
pollScheduler.updatePolling(config);
|
||||
} catch (Exception e) {
|
||||
log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 清理已删除设备的资源
|
||||
Set<Long> removedDeviceIds = configCacheService.cleanupRemovedDevices(configs);
|
||||
for (Long deviceId : removedDeviceIds) {
|
||||
pollScheduler.stopPolling(deviceId);
|
||||
connectionManager.removeDevice(deviceId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[refreshConfig][刷新配置失败]", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT Modbus TCP Client 下行消息处理器
|
||||
* <p>
|
||||
* 负责:
|
||||
* 1. 处理下行消息(如属性设置 thing.service.property.set)
|
||||
* 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class IotModbusTcpClientDownstreamHandler {
|
||||
|
||||
private final IotModbusTcpClientConnectionManager connectionManager;
|
||||
private final IotModbusTcpClientConfigCacheService configCacheService;
|
||||
|
||||
/**
|
||||
* 处理下行消息
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "DuplicatedCode"})
|
||||
public void handle(IotDeviceMessage message) {
|
||||
// 1.1 检查是否是属性设置消息
|
||||
if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) {
|
||||
return;
|
||||
}
|
||||
if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) {
|
||||
log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod());
|
||||
return;
|
||||
}
|
||||
// 1.2 获取设备配置
|
||||
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId());
|
||||
if (config == null) {
|
||||
log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 解析属性值并写入
|
||||
Object params = message.getParams();
|
||||
if (!(params instanceof Map)) {
|
||||
log.warn("[handle][params 不是 Map 类型: {}]", params);
|
||||
return;
|
||||
}
|
||||
Map<String, Object> propertyMap = (Map<String, Object>) params;
|
||||
for (Map.Entry<String, Object> entry : propertyMap.entrySet()) {
|
||||
String identifier = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
// 2.1 查找对应的点位配置
|
||||
IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier);
|
||||
if (point == null) {
|
||||
log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
|
||||
continue;
|
||||
}
|
||||
// 2.2 检查是否支持写操作
|
||||
if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) {
|
||||
log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2.3 执行写入
|
||||
writeProperty(config, point, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入属性值
|
||||
*/
|
||||
private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) {
|
||||
// 1.1 获取连接
|
||||
IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId());
|
||||
if (connection == null) {
|
||||
log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId());
|
||||
return;
|
||||
}
|
||||
// 1.2 获取 slave ID
|
||||
Integer slaveId = connectionManager.getSlaveId(config.getDeviceId());
|
||||
if (slaveId == null) {
|
||||
log.warn("[writeProperty][设备 {} 没有 slaveId]", config.getDeviceId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 转换属性值为原始值
|
||||
int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point);
|
||||
// 2.2 执行 Modbus 写入
|
||||
IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues)
|
||||
.onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
|
||||
config.getDeviceId(), point.getIdentifier(), value))
|
||||
.onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]",
|
||||
config.getDeviceId(), point.getIdentifier(), e));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotModbusTcpClientDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
|
||||
|
||||
private final IotModbusTcpClientDownstreamHandler downstreamHandler;
|
||||
|
||||
public IotModbusTcpClientDownstreamSubscriber(IotModbusTcpClientProtocol protocol,
|
||||
IotModbusTcpClientDownstreamHandler downstreamHandler,
|
||||
IotMessageBus messageBus) {
|
||||
super(protocol, messageBus);
|
||||
this.downstreamHandler = downstreamHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMessage(IotDeviceMessage message) {
|
||||
downstreamHandler.handle(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT Modbus TCP 上行数据处理器:将原始值转换为物模型属性值并上报
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotModbusTcpClientUpstreamHandler {
|
||||
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
private final String serverId;
|
||||
|
||||
public IotModbusTcpClientUpstreamHandler(IotDeviceMessageService messageService,
|
||||
String serverId) {
|
||||
this.messageService = messageService;
|
||||
this.serverId = serverId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Modbus 读取结果
|
||||
*
|
||||
* @param config 设备配置
|
||||
* @param point 点位配置
|
||||
* @param rawValue 原始值(int 数组)
|
||||
*/
|
||||
public void handleReadResult(IotModbusDeviceConfigRespDTO config,
|
||||
IotModbusPointRespDTO point,
|
||||
int[] rawValue) {
|
||||
try {
|
||||
// 1.1 转换原始值为物模型属性值(点位翻译)
|
||||
Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValue, point);
|
||||
log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]",
|
||||
config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue);
|
||||
// 1.2 构造属性上报消息
|
||||
Map<String, Object> params = MapUtil.of(point.getIdentifier(), convertedValue);
|
||||
IotDeviceMessage message = IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
|
||||
|
||||
// 2. 发送到消息总线
|
||||
messageService.sendDeviceMessage(message, config.getProductKey(),
|
||||
config.getDeviceName(), serverId);
|
||||
} catch (Exception e) {
|
||||
log.error("[handleReadResult][处理读取结果失败, deviceId={}, identifier={}]",
|
||||
config.getDeviceId(), point.getIdentifier(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
|
||||
|
||||
/**
|
||||
* IoT Modbus TCP Client 配置缓存服务
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class IotModbusTcpClientConfigCacheService {
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
/**
|
||||
* 配置缓存:deviceId -> 配置
|
||||
*/
|
||||
private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 已知的设备 ID 集合(作用:用于检测已删除的设备)
|
||||
*
|
||||
* @see #cleanupRemovedDevices(List)
|
||||
*/
|
||||
private final Set<Long> knownDeviceIds = ConcurrentHashMap.newKeySet();
|
||||
|
||||
/**
|
||||
* 刷新配置
|
||||
*
|
||||
* @return 最新的配置列表;API 失败时返回 null(调用方应跳过 cleanup)
|
||||
*/
|
||||
public List<IotModbusDeviceConfigRespDTO> refreshConfig() {
|
||||
try {
|
||||
// 1. 从远程获取配置
|
||||
CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(
|
||||
new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus())
|
||||
.setMode(IotModbusModeEnum.POLLING.getMode()).setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_CLIENT.getType()));
|
||||
result.checkError();
|
||||
List<IotModbusDeviceConfigRespDTO> configs = result.getData();
|
||||
|
||||
// 2. 更新缓存(注意:不在这里更新 knownDeviceIds,由 cleanupRemovedDevices 统一管理)
|
||||
for (IotModbusDeviceConfigRespDTO config : configs) {
|
||||
configCache.put(config.getDeviceId(), config);
|
||||
}
|
||||
return configs;
|
||||
} catch (Exception e) {
|
||||
log.error("[refreshConfig][刷新配置失败]", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备配置
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @return 配置
|
||||
*/
|
||||
public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) {
|
||||
return configCache.get(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算已删除设备的 ID 集合,清理缓存,并更新已知设备 ID 集合
|
||||
*
|
||||
* @param currentConfigs 当前有效的配置列表
|
||||
* @return 已删除的设备 ID 集合
|
||||
*/
|
||||
public Set<Long> cleanupRemovedDevices(List<IotModbusDeviceConfigRespDTO> currentConfigs) {
|
||||
// 1.1 获取当前有效的设备 ID
|
||||
Set<Long> currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId);
|
||||
// 1.2 找出已删除的设备(基于旧的 knownDeviceIds)
|
||||
Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds);
|
||||
removedDeviceIds.removeAll(currentDeviceIds);
|
||||
|
||||
// 2. 清理已删除设备的缓存
|
||||
for (Long deviceId : removedDeviceIds) {
|
||||
log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId);
|
||||
configCache.remove(deviceId);
|
||||
}
|
||||
|
||||
// 3. 更新已知设备 ID 集合为当前有效的设备 ID
|
||||
knownDeviceIds.clear();
|
||||
knownDeviceIds.addAll(currentDeviceIds);
|
||||
return removedDeviceIds;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import com.ghgande.j2mod.modbus.net.TCPMasterConnection;
|
||||
import io.vertx.core.Context;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Vertx;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* IoT Modbus TCP 连接管理器
|
||||
* <p>
|
||||
* 统一管理 Modbus TCP 连接:
|
||||
* 1. 管理 TCP 连接(相同 ip:port 共用连接)
|
||||
* 2. 分布式锁管理(连接级别),避免多节点重复创建连接
|
||||
* 3. 连接重试和故障恢复
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotModbusTcpClientConnectionManager {
|
||||
|
||||
private static final String LOCK_KEY_PREFIX = "iot:modbus-tcp:connection:";
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
private final Vertx vertx;
|
||||
private final IotDeviceMessageService messageService;
|
||||
private final IotModbusTcpClientConfigCacheService configCacheService;
|
||||
private final String serverId;
|
||||
|
||||
/**
|
||||
* 连接池:key = ip:port
|
||||
*/
|
||||
private final Map<String, ModbusConnection> connectionPool = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 设备 ID 到连接 key 的映射
|
||||
*/
|
||||
private final Map<Long, String> deviceConnectionMap = new ConcurrentHashMap<>();
|
||||
|
||||
public IotModbusTcpClientConnectionManager(RedissonClient redissonClient, Vertx vertx,
|
||||
IotDeviceMessageService messageService,
|
||||
IotModbusTcpClientConfigCacheService configCacheService,
|
||||
String serverId) {
|
||||
this.redissonClient = redissonClient;
|
||||
this.vertx = vertx;
|
||||
this.messageService = messageService;
|
||||
this.configCacheService = configCacheService;
|
||||
this.serverId = serverId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保连接存在
|
||||
* <p>
|
||||
* 首次建连成功时,直接发送设备上线消息
|
||||
*
|
||||
* @param config 设备配置
|
||||
*/
|
||||
public void ensureConnection(IotModbusDeviceConfigRespDTO config) {
|
||||
// 1.1 检查设备是否切换了 IP/端口,若是则先清理旧连接
|
||||
String connectionKey = buildConnectionKey(config.getIp(), config.getPort());
|
||||
String oldConnectionKey = deviceConnectionMap.get(config.getDeviceId());
|
||||
if (oldConnectionKey != null && ObjUtil.notEqual(oldConnectionKey, connectionKey)) {
|
||||
log.info("[ensureConnection][设备 {} IP/端口变更: {} -> {}, 清理旧连接]",
|
||||
config.getDeviceId(), oldConnectionKey, connectionKey);
|
||||
removeDevice(config.getDeviceId());
|
||||
}
|
||||
// 1.2 记录设备与连接的映射
|
||||
deviceConnectionMap.put(config.getDeviceId(), connectionKey);
|
||||
|
||||
// 2. 情况一:连接已存在,注册设备并发送上线消息
|
||||
ModbusConnection connection = connectionPool.get(connectionKey);
|
||||
if (connection != null) {
|
||||
addDeviceAndOnline(connection, config);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 情况二:连接不存在,加分布式锁创建新连接
|
||||
RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + connectionKey);
|
||||
if (!lock.tryLock()) {
|
||||
log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 3.1 double-check:拿到锁后再次检查,避免并发创建重复连接
|
||||
connection = connectionPool.get(connectionKey);
|
||||
if (connection != null) {
|
||||
addDeviceAndOnline(connection, config);
|
||||
lock.unlock();
|
||||
return;
|
||||
}
|
||||
// 3.2 创建新连接
|
||||
connection = createConnection(config);
|
||||
connection.setLock(lock);
|
||||
connectionPool.put(connectionKey, connection);
|
||||
log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey);
|
||||
// 3.3 注册设备并发送上线消息
|
||||
addDeviceAndOnline(connection, config);
|
||||
} catch (Exception e) {
|
||||
log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e);
|
||||
// 建连失败,释放锁让其他节点可重试
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Modbus TCP 连接
|
||||
*/
|
||||
private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config) throws Exception {
|
||||
// 1. 创建 TCP 连接
|
||||
TCPMasterConnection tcpConnection = new TCPMasterConnection(InetAddress.getByName(config.getIp()));
|
||||
tcpConnection.setPort(config.getPort());
|
||||
tcpConnection.setTimeout(config.getTimeout());
|
||||
tcpConnection.connect();
|
||||
|
||||
// 2. 创建 Modbus 连接对象
|
||||
return new ModbusConnection()
|
||||
.setConnectionKey(buildConnectionKey(config.getIp(), config.getPort()))
|
||||
.setTcpConnection(tcpConnection).setContext(vertx.getOrCreateContext())
|
||||
.setTimeout(config.getTimeout()).setRetryInterval(config.getRetryInterval());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接
|
||||
*/
|
||||
public ModbusConnection getConnection(Long deviceId) {
|
||||
String connectionKey = deviceConnectionMap.get(deviceId);
|
||||
if (connectionKey == null) {
|
||||
return null;
|
||||
}
|
||||
return connectionPool.get(connectionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备的 slave ID
|
||||
*/
|
||||
public Integer getSlaveId(Long deviceId) {
|
||||
ModbusConnection connection = getConnection(deviceId);
|
||||
if (connection == null) {
|
||||
return null;
|
||||
}
|
||||
return connection.getSlaveId(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除设备
|
||||
* <p>
|
||||
* 移除时直接发送设备下线消息
|
||||
*/
|
||||
public void removeDevice(Long deviceId) {
|
||||
// 1.1 移除设备时,发送下线消息
|
||||
sendOfflineMessage(deviceId);
|
||||
// 1.2 移除设备引用
|
||||
String connectionKey = deviceConnectionMap.remove(deviceId);
|
||||
if (connectionKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 移除连接中的设备引用
|
||||
ModbusConnection connection = connectionPool.get(connectionKey);
|
||||
if (connection == null) {
|
||||
return;
|
||||
}
|
||||
connection.removeDevice(deviceId);
|
||||
// 2.2 如果没有设备引用了,关闭连接
|
||||
if (connection.getDeviceCount() == 0) {
|
||||
closeConnection(connectionKey);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 设备连接 & 上下线消息 ====================
|
||||
|
||||
/**
|
||||
* 注册设备到连接,并发送上线消息
|
||||
*/
|
||||
private void addDeviceAndOnline(ModbusConnection connection,
|
||||
IotModbusDeviceConfigRespDTO config) {
|
||||
Integer previous = connection.addDevice(config.getDeviceId(), config.getSlaveId());
|
||||
// 首次注册,发送上线消息
|
||||
if (previous == null) {
|
||||
sendOnlineMessage(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送设备上线消息
|
||||
*/
|
||||
private void sendOnlineMessage(IotModbusDeviceConfigRespDTO config) {
|
||||
try {
|
||||
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
|
||||
messageService.sendDeviceMessage(onlineMessage,
|
||||
config.getProductKey(), config.getDeviceName(), serverId);
|
||||
} catch (Exception ex) {
|
||||
log.error("[sendOnlineMessage][发送设备上线消息失败, deviceId={}]", config.getDeviceId(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送设备下线消息
|
||||
*/
|
||||
private void sendOfflineMessage(Long deviceId) {
|
||||
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
|
||||
if (config == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
|
||||
messageService.sendDeviceMessage(offlineMessage,
|
||||
config.getProductKey(), config.getDeviceName(), serverId);
|
||||
} catch (Exception ex) {
|
||||
log.error("[sendOfflineMessage][发送设备下线消息失败, deviceId={}]", deviceId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭指定连接
|
||||
*/
|
||||
private void closeConnection(String connectionKey) {
|
||||
ModbusConnection connection = connectionPool.remove(connectionKey);
|
||||
if (connection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (connection.getTcpConnection() != null) {
|
||||
connection.getTcpConnection().close();
|
||||
}
|
||||
// 释放分布式锁,让其他节点可接管
|
||||
RLock lock = connection.getLock();
|
||||
if (lock != null && lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey);
|
||||
} catch (Exception e) {
|
||||
log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有连接
|
||||
*/
|
||||
public void closeAll() {
|
||||
// 先复制再遍历,避免 closeConnection 中 remove 导致并发修改
|
||||
List<String> connectionKeys = new ArrayList<>(connectionPool.keySet());
|
||||
for (String connectionKey : connectionKeys) {
|
||||
closeConnection(connectionKey);
|
||||
}
|
||||
deviceConnectionMap.clear();
|
||||
}
|
||||
|
||||
private String buildConnectionKey(String ip, Integer port) {
|
||||
return ip + ":" + port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modbus 连接信息
|
||||
*/
|
||||
@Data
|
||||
public static class ModbusConnection {
|
||||
|
||||
private String connectionKey;
|
||||
private TCPMasterConnection tcpConnection;
|
||||
private Integer timeout;
|
||||
private Integer retryInterval;
|
||||
/**
|
||||
* 设备 ID 到 slave ID 的映射
|
||||
*/
|
||||
private final Map<Long, Integer> deviceSlaveMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 分布式锁,锁住连接的创建和销毁,避免多节点重复连接同一从站
|
||||
*/
|
||||
private RLock lock;
|
||||
|
||||
/**
|
||||
* Vert.x Context,用于 executeBlocking 执行 Modbus 操作,保证同一连接的操作串行执行
|
||||
*/
|
||||
private Context context;
|
||||
|
||||
public Integer addDevice(Long deviceId, Integer slaveId) {
|
||||
return deviceSlaveMap.putIfAbsent(deviceId, slaveId);
|
||||
}
|
||||
|
||||
public void removeDevice(Long deviceId) {
|
||||
deviceSlaveMap.remove(deviceId);
|
||||
}
|
||||
|
||||
public int getDeviceCount() {
|
||||
return deviceSlaveMap.size();
|
||||
}
|
||||
|
||||
public Integer getSlaveId(Long deviceId) {
|
||||
return deviceSlaveMap.get(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Modbus 读取操作(阻塞方式,在 Vert.x worker 线程执行)
|
||||
*/
|
||||
public <T> Future<T> executeBlocking(java.util.function.Function<TCPMasterConnection, T> operation) {
|
||||
// ordered=true 保证同一 Context 的操作串行执行,不同连接之间可并行
|
||||
return context.executeBlocking(() -> operation.apply(tcpConnection), true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler;
|
||||
import io.vertx.core.Vertx;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT Modbus TCP Client 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotModbusTcpClientPollScheduler extends AbstractIotModbusPollScheduler {
|
||||
|
||||
private final IotModbusTcpClientConnectionManager connectionManager;
|
||||
private final IotModbusTcpClientUpstreamHandler upstreamHandler;
|
||||
private final IotModbusTcpClientConfigCacheService configCacheService;
|
||||
|
||||
public IotModbusTcpClientPollScheduler(Vertx vertx,
|
||||
IotModbusTcpClientConnectionManager connectionManager,
|
||||
IotModbusTcpClientUpstreamHandler upstreamHandler,
|
||||
IotModbusTcpClientConfigCacheService configCacheService) {
|
||||
super(vertx);
|
||||
this.connectionManager = connectionManager;
|
||||
this.upstreamHandler = upstreamHandler;
|
||||
this.configCacheService = configCacheService;
|
||||
}
|
||||
|
||||
// ========== 轮询执行 ==========
|
||||
|
||||
/**
|
||||
* 轮询单个点位
|
||||
*/
|
||||
@Override
|
||||
protected void pollPoint(Long deviceId, Long pointId) {
|
||||
// 1.1 从 configCache 获取最新配置
|
||||
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
|
||||
if (config == null || CollUtil.isEmpty(config.getPoints())) {
|
||||
log.warn("[pollPoint][设备 {} 没有配置]", deviceId);
|
||||
return;
|
||||
}
|
||||
// 1.2 查找点位
|
||||
IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId);
|
||||
if (point == null) {
|
||||
log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 获取连接
|
||||
IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId);
|
||||
if (connection == null) {
|
||||
log.warn("[pollPoint][设备 {} 没有连接]", deviceId);
|
||||
return;
|
||||
}
|
||||
// 2.2 获取 slave ID
|
||||
Integer slaveId = connectionManager.getSlaveId(deviceId);
|
||||
Assert.notNull(slaveId, "设备 {} 没有配置 slaveId", deviceId);
|
||||
|
||||
// 3. 执行 Modbus 读取
|
||||
IotModbusTcpClientUtils.read(connection, slaveId, point)
|
||||
.onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue))
|
||||
.onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]",
|
||||
deviceId, point.getIdentifier(), e));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Modbus TCP Client(主站)协议:网关主动连接并轮询 Modbus 从站设备
|
||||
* <p>
|
||||
* 基于 j2mod 实现,支持 FC01-04 读、FC05/06/15/16 写,定时轮询 + 下发属性设置
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
|
||||
@@ -0,0 +1,44 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT Modbus TCP Server 协议配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class IotModbusTcpServerConfig {
|
||||
|
||||
/**
|
||||
* 配置刷新间隔(秒)
|
||||
*/
|
||||
@NotNull(message = "配置刷新间隔不能为空")
|
||||
@Min(value = 1, message = "配置刷新间隔不能小于 1 秒")
|
||||
private Integer configRefreshInterval = 30;
|
||||
|
||||
/**
|
||||
* 自定义功能码(用于认证等扩展交互)
|
||||
* Modbus 协议保留 65-72 给用户自定义,默认 65
|
||||
*/
|
||||
@NotNull(message = "自定义功能码不能为空")
|
||||
@Min(value = 65, message = "自定义功能码不能小于 65")
|
||||
@Max(value = 72, message = "自定义功能码不能大于 72")
|
||||
private Integer customFunctionCode = 65;
|
||||
|
||||
/**
|
||||
* Pending Request 超时时间(毫秒)
|
||||
*/
|
||||
@NotNull(message = "请求超时时间不能为空")
|
||||
private Integer requestTimeout = 5000;
|
||||
|
||||
/**
|
||||
* Pending Request 清理间隔(毫秒)
|
||||
*/
|
||||
@NotNull(message = "请求清理间隔不能为空")
|
||||
private Integer requestCleanupInterval = 10000;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.upstream.IotModbusTcpServerUpstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPollScheduler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.net.NetServer;
|
||||
import io.vertx.core.net.NetServerOptions;
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import io.vertx.core.parsetools.RecordParser;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* IoT 网关 Modbus TCP Server 协议
|
||||
* <p>
|
||||
* 作为 TCP Server 接收设备主动连接:
|
||||
* 1. 设备通过自定义功能码(FC 65)发送认证请求
|
||||
* 2. 认证成功后,网关主动发送 Modbus 读请求,设备响应(云端轮询模式)
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotModbusTcpServerProtocol implements IotProtocol {
|
||||
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
private final ProtocolProperties properties;
|
||||
/**
|
||||
* 服务器 ID(用于消息追踪,全局唯一)
|
||||
*/
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
/**
|
||||
* 运行状态
|
||||
*/
|
||||
@Getter
|
||||
private volatile boolean running = false;
|
||||
|
||||
/**
|
||||
* Vert.x 实例
|
||||
*/
|
||||
private final Vertx vertx;
|
||||
/**
|
||||
* TCP Server
|
||||
*/
|
||||
private NetServer netServer;
|
||||
/**
|
||||
* 配置刷新定时器 ID
|
||||
*/
|
||||
private Long configRefreshTimerId;
|
||||
/**
|
||||
* Pending Request 清理定时器 ID
|
||||
*/
|
||||
private Long requestCleanupTimerId;
|
||||
|
||||
/**
|
||||
* 连接管理器
|
||||
*/
|
||||
private final IotModbusTcpServerConnectionManager connectionManager;
|
||||
/**
|
||||
* 下行消息订阅者
|
||||
*/
|
||||
private IotModbusTcpServerDownstreamSubscriber downstreamSubscriber;
|
||||
|
||||
private final IotModbusFrameDecoder frameDecoder;
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final IotModbusFrameEncoder frameEncoder;
|
||||
|
||||
private final IotModbusTcpServerConfigCacheService configCacheService;
|
||||
private final IotModbusTcpServerPendingRequestManager pendingRequestManager;
|
||||
private final IotModbusTcpServerUpstreamHandler upstreamHandler;
|
||||
private final IotModbusTcpServerPollScheduler pollScheduler;
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
public IotModbusTcpServerProtocol(ProtocolProperties properties) {
|
||||
IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer();
|
||||
Assert.notNull(slaveConfig, "Modbus TCP Server 协议配置(modbusTcpServer)不能为空");
|
||||
this.properties = properties;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
|
||||
|
||||
// 初始化 Vertx
|
||||
this.vertx = Vertx.vertx();
|
||||
|
||||
// 初始化 Manager
|
||||
IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
this.connectionManager = new IotModbusTcpServerConnectionManager();
|
||||
this.configCacheService = new IotModbusTcpServerConfigCacheService(deviceApi);
|
||||
this.pendingRequestManager = new IotModbusTcpServerPendingRequestManager();
|
||||
|
||||
// 初始化帧编解码器
|
||||
this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode());
|
||||
this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode());
|
||||
|
||||
// 初始化共享事务 ID 自增器(PollScheduler 和 DownstreamHandler 共用,避免 transactionId 冲突)
|
||||
AtomicInteger transactionIdCounter = new AtomicInteger(0);
|
||||
// 初始化轮询调度器
|
||||
this.pollScheduler = new IotModbusTcpServerPollScheduler(
|
||||
vertx, connectionManager, frameEncoder, pendingRequestManager,
|
||||
slaveConfig.getRequestTimeout(), transactionIdCounter, configCacheService);
|
||||
|
||||
// 初始化 Handler
|
||||
this.messageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class);
|
||||
this.upstreamHandler = new IotModbusTcpServerUpstreamHandler(
|
||||
deviceApi, this.messageService, frameEncoder,
|
||||
connectionManager, configCacheService, pendingRequestManager,
|
||||
pollScheduler, deviceService, serverId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return properties.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotProtocolTypeEnum getType() {
|
||||
return IotProtocolTypeEnum.MODBUS_TCP_SERVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (running) {
|
||||
log.warn("[start][IoT Modbus TCP Server 协议 {} 已经在运行中]", getId());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 启动配置刷新定时器
|
||||
IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer();
|
||||
configRefreshTimerId = vertx.setPeriodic(
|
||||
TimeUnit.SECONDS.toMillis(slaveConfig.getConfigRefreshInterval()),
|
||||
id -> refreshConfig());
|
||||
|
||||
// 2.1 启动 TCP Server
|
||||
startTcpServer();
|
||||
|
||||
// 2.2 启动 PendingRequest 清理定时器
|
||||
requestCleanupTimerId = vertx.setPeriodic(
|
||||
slaveConfig.getRequestCleanupInterval(),
|
||||
id -> pendingRequestManager.cleanupExpired());
|
||||
running = true;
|
||||
log.info("[start][IoT Modbus TCP Server 协议 {} 启动成功, serverId={}, port={}]",
|
||||
getId(), serverId, properties.getPort());
|
||||
|
||||
// 3. 启动下行消息订阅
|
||||
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
|
||||
IotModbusTcpServerDownstreamHandler downstreamHandler = new IotModbusTcpServerDownstreamHandler(
|
||||
connectionManager, configCacheService, frameEncoder, this.pollScheduler.getTransactionIdCounter());
|
||||
this.downstreamSubscriber = new IotModbusTcpServerDownstreamSubscriber(
|
||||
this, downstreamHandler, messageBus);
|
||||
downstreamSubscriber.start();
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT Modbus TCP Server 协议 {} 启动失败]", getId(), e);
|
||||
stop0();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
stop0();
|
||||
}
|
||||
|
||||
private void stop0() {
|
||||
// 1. 停止下行消息订阅
|
||||
if (downstreamSubscriber != null) {
|
||||
try {
|
||||
downstreamSubscriber.stop();
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][下行消息订阅器停止失败]", e);
|
||||
}
|
||||
downstreamSubscriber = null;
|
||||
}
|
||||
|
||||
// 2.1 取消定时器
|
||||
if (configRefreshTimerId != null) {
|
||||
vertx.cancelTimer(configRefreshTimerId);
|
||||
configRefreshTimerId = null;
|
||||
}
|
||||
if (requestCleanupTimerId != null) {
|
||||
vertx.cancelTimer(requestCleanupTimerId);
|
||||
requestCleanupTimerId = null;
|
||||
}
|
||||
// 2.2 停止轮询
|
||||
pollScheduler.stopAll();
|
||||
// 2.3 清理 PendingRequest
|
||||
pendingRequestManager.clear();
|
||||
// 2.4 关闭所有连接
|
||||
connectionManager.closeAll();
|
||||
// 2.5 关闭 TCP Server
|
||||
if (netServer != null) {
|
||||
try {
|
||||
netServer.close().result();
|
||||
log.info("[stop][TCP Server 已关闭]");
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][TCP Server 关闭失败]", e);
|
||||
}
|
||||
netServer = null;
|
||||
}
|
||||
|
||||
// 3. 关闭 Vertx
|
||||
if (vertx != null) {
|
||||
try {
|
||||
vertx.close().result();
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][Vertx 关闭失败]", e);
|
||||
}
|
||||
}
|
||||
running = false;
|
||||
log.info("[stop][IoT Modbus TCP Server 协议 {} 已停止]", getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 TCP Server
|
||||
*/
|
||||
private void startTcpServer() {
|
||||
// 1. 创建 TCP Server
|
||||
NetServerOptions options = new NetServerOptions()
|
||||
.setPort(properties.getPort());
|
||||
netServer = vertx.createNetServer(options);
|
||||
|
||||
// 2. 设置连接处理器
|
||||
netServer.connectHandler(this::handleConnection);
|
||||
try {
|
||||
netServer.listen().toCompletionStage().toCompletableFuture().get();
|
||||
log.info("[startTcpServer][TCP Server 启动成功, port={}]", properties.getPort());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("[startTcpServer][TCP Server 启动失败]", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理新连接
|
||||
*/
|
||||
private void handleConnection(NetSocket socket) {
|
||||
log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress());
|
||||
|
||||
// 1. 创建 RecordParser 并设置为数据处理器
|
||||
RecordParser recordParser = frameDecoder.createRecordParser((frame, frameFormat) -> {
|
||||
// 【重要】帧处理分发,即消息处理
|
||||
upstreamHandler.handleFrame(socket, frame, frameFormat);
|
||||
});
|
||||
socket.handler(recordParser);
|
||||
|
||||
// 2.1 连接关闭处理
|
||||
socket.closeHandler(v -> {
|
||||
ConnectionInfo info = connectionManager.removeConnection(socket);
|
||||
if (info == null || info.getDeviceId() == null) {
|
||||
log.info("[handleConnection][未认证连接关闭, remoteAddress={}]", socket.remoteAddress());
|
||||
return;
|
||||
}
|
||||
pollScheduler.stopPolling(info.getDeviceId());
|
||||
pendingRequestManager.removeDevice(info.getDeviceId());
|
||||
configCacheService.removeConfig(info.getDeviceId());
|
||||
// 发送设备下线消息
|
||||
try {
|
||||
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
|
||||
messageService.sendDeviceMessage(offlineMessage, info.getProductKey(), info.getDeviceName(), serverId);
|
||||
} catch (Exception ex) {
|
||||
log.error("[handleConnection][发送设备下线消息失败, deviceId={}]", info.getDeviceId(), ex);
|
||||
}
|
||||
log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]",
|
||||
info.getDeviceId(), socket.remoteAddress());
|
||||
});
|
||||
// 2.2 异常处理
|
||||
socket.exceptionHandler(e -> {
|
||||
log.error("[handleConnection][连接异常, remoteAddress={}]", socket.remoteAddress(), e);
|
||||
socket.close();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新已连接设备的配置(定时调用)
|
||||
*/
|
||||
private synchronized void refreshConfig() {
|
||||
try {
|
||||
// 1. 只刷新已连接设备的配置
|
||||
Set<Long> connectedDeviceIds = connectionManager.getConnectedDeviceIds();
|
||||
if (CollUtil.isEmpty(connectedDeviceIds)) {
|
||||
return;
|
||||
}
|
||||
List<IotModbusDeviceConfigRespDTO> configs =
|
||||
configCacheService.refreshConnectedDeviceConfigList(connectedDeviceIds);
|
||||
if (configs == null) {
|
||||
log.warn("[refreshConfig][刷新配置失败,跳过本次刷新]");
|
||||
return;
|
||||
}
|
||||
log.debug("[refreshConfig][刷新了 {} 个已连接设备的配置]", configs.size());
|
||||
|
||||
// 2. 更新已连接设备的轮询任务
|
||||
for (IotModbusDeviceConfigRespDTO config : configs) {
|
||||
try {
|
||||
pollScheduler.updatePolling(config);
|
||||
} catch (Exception e) {
|
||||
log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[refreshConfig][刷新配置失败]", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* IoT Modbus 统一帧数据模型(TCP/RTU 公用)
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class IotModbusFrame {
|
||||
|
||||
/**
|
||||
* 从站地址
|
||||
*/
|
||||
private int slaveId;
|
||||
/**
|
||||
* 功能码
|
||||
*/
|
||||
private int functionCode;
|
||||
/**
|
||||
* PDU 数据(不含 slaveId)
|
||||
*/
|
||||
private byte[] pdu;
|
||||
/**
|
||||
* 事务标识符
|
||||
* <p>
|
||||
* 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值
|
||||
*/
|
||||
private Integer transactionId;
|
||||
|
||||
/**
|
||||
* 异常码
|
||||
* <p>
|
||||
* 当功能码最高位为 1 时(异常响应),此字段存储异常码。
|
||||
*
|
||||
* @see IotModbusCommonUtils#FC_EXCEPTION_MASK
|
||||
*/
|
||||
private Integer exceptionCode;
|
||||
|
||||
/**
|
||||
* 自定义功能码时的 JSON 字符串(用于 auth 认证等等)
|
||||
*/
|
||||
private String customData;
|
||||
|
||||
/**
|
||||
* 是否异常响应(基于 exceptionCode 是否有值判断)
|
||||
*/
|
||||
public boolean isException() {
|
||||
return exceptionCode != null;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user