!1518 接入 modbus 协议,重构所有协议的配置

Merge pull request !1518 from 芋道源码/feature/iot
This commit is contained in:
芋道源码
2026-02-14 03:19:51 +00:00
committed by Gitee
241 changed files with 14885 additions and 7828 deletions

View File

@@ -101,18 +101,6 @@
<artifactId>spring-boot-starter-amqp</artifactId>
<optional>true</optional>
</dependency>
<!-- IoT 网络组件:接收来自设备的上行数据 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-iot-net-component-http</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-iot-net-component-emqx</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
</dependencies>
</project>

View File

@@ -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")
@@ -58,11 +74,57 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> {
IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId());
if (product != null) {
deviceDTO.setCodecType(product.getCodecType());
deviceDTO.setProtocolType(product.getProtocolType()).setSerializeType(product.getSerializeType());
}
}));
}
@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

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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;

View File

@@ -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 = "请求参数")

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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}}

View File

@@ -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")

View File

@@ -67,10 +67,15 @@ public class IotProductRespVO {
@DictFormat(DictTypeConstants.NET_TYPE)
private Integer netType;
@Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@ExcelProperty(value = "数据格式", converter = DictConvert.class)
@DictFormat(DictTypeConstants.CODEC_TYPE)
private String codecType;
@Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt")
@ExcelProperty(value = "协议类型", converter = DictConvert.class)
@DictFormat(DictTypeConstants.PROTOCOL_TYPE)
private String protocolType;
@Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json")
@ExcelProperty(value = "序列化类型", converter = DictConvert.class)
@DictFormat(DictTypeConstants.SERIALIZE_TYPE)
private String serializeType;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("创建时间")

View File

@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -44,9 +46,15 @@ public class IotProductSaveReqVO {
@InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}")
private Integer netType;
@Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@NotEmpty(message = "数据格式不能为空")
private String codecType;
@Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt")
@InEnum(value = IotProtocolTypeEnum.class, message = "协议类型必须是 {value}")
@NotEmpty(message = "协议类型不能为空")
private String protocolType;
@Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json")
@InEnum(value = IotSerializeTypeEnum.class, message = "序列化类型必须是 {value}")
@NotEmpty(message = "序列化类型不能为空")
private String serializeType;
@Schema(description = "是否开启动态注册", example = "false")
@NotNull(message = "是否开启动态注册不能为空")

View File

@@ -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;

View File

@@ -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;
/**
* 固件编号
*

View File

@@ -84,7 +84,7 @@ public class IotDeviceMessageDO {
* 请求方法
*
* 枚举 {@link IotDeviceMessageMethodEnum}
* 例如说thing.property.report 属性上报
* 例如说thing.property.post 属性上报
*/
private String method;
/**

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -78,12 +78,16 @@ public class IotProductDO extends TenantBaseDO {
*/
private Integer netType;
/**
* 数据格式(编解码器类型
* 协议类型
* <p>
* 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE}
*
* 目的:用于 gateway-server 解析消息格式
* 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum}
*/
private String codecType;
private String protocolType;
/**
* 序列化类型
* <p>
* 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum}
*/
private String serializeType;
}

View File

@@ -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()));
}
}

View File

@@ -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));
}
}

View File

@@ -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));

View File

@@ -8,8 +8,8 @@ package cn.iocoder.yudao.module.iot.enums;
public class DictTypeConstants {
public static final String NET_TYPE = "iot_net_type";
public static final String LOCATION_TYPE = "iot_location_type";
public static final String CODEC_TYPE = "iot_codec_type";
public static final String PROTOCOL_TYPE = "iot_protocol_type";
public static final String SERIALIZE_TYPE = "iot_serialize_type";
public static final String PRODUCT_STATUS = "iot_product_status";
public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type";

View File

@@ -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, "固件信息不存在");

View File

@@ -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"),

View File

@@ -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;

View File

@@ -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;

View File

@@ -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. 未上线的设备,强制上线

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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 模式下,数据帧格式不能为空");
}
}
}

View File

@@ -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 设备点位 Mapkey 为设备编号value 为点位配置列表
*/
Map<Long, List<IotDeviceModbusPointDO>> getEnabledDeviceModbusPointMapByDeviceIds(Collection<Long> deviceIds);
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;
@@ -29,6 +29,7 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
@@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService {
if (BooleanUtil.isFalse(product.getRegisterEnabled())) {
throw exception(DEVICE_REGISTER_DISABLED);
}
// 1.3 验证 productSecret
if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) {
// 1.3 【重要!!!】验证签名
if (!IotProductAuthUtils.verifySign(reqDTO.getProductKey(), reqDTO.getDeviceName(),
product.getProductSecret(), reqDTO.getSign())) {
throw exception(DEVICE_REGISTER_SECRET_INVALID);
}
return TenantUtils.execute(product.getTenantId(), () -> {

View File

@@ -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 升级记录

View File

@@ -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 产品 Mapkey: 产品编号, 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();
}

View File

@@ -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)) {

View File

@@ -14,8 +14,6 @@ import java.time.Duration;
// TODO @芋艿:数据库
// TODO @芋艿mqtt
// TODO @芋艿tcp
// TODO @芋艿websocket
/**
* 可缓存的 {@link IotDataRuleAction} 抽象实现

View File

@@ -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;

View File

@@ -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());
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -34,8 +34,12 @@ public class IotDeviceRespDTO {
*/
private Long productId;
/**
* 编解码器类型
* 协议类型
*/
private String codecType;
private String protocolType;
/**
* 序列化类型
*/
private String serializeType;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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),
;

View File

@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.iot.core.enums;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT 协议类型枚举
*
* 用于定义传输层协议类型
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Getter
public enum IotProtocolTypeEnum implements ArrayValuable<String> {
TCP("tcp"),
UDP("udp"),
WEBSOCKET("websocket"),
HTTP("http"),
MQTT("mqtt"),
EMQX("emqx"),
COAP("coap"),
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);
/**
* 类型
*/
private final String type;
@Override
public String[] array() {
return ARRAYS;
}
public static IotProtocolTypeEnum of(String type) {
return ArrayUtil.firstMatch(e -> e.getType().equals(type), values());
}
}

View File

@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.iot.core.enums;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT 序列化类型枚举
*
* 用于定义设备消息的序列化格式
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Getter
public enum IotSerializeTypeEnum implements ArrayValuable<String> {
JSON("json"),
BINARY("binary");
public static final String[] ARRAYS = Arrays.stream(values()).map(IotSerializeTypeEnum::getType).toArray(String[]::new);
/**
* 类型
*/
private final String type;
@Override
public String[] array() {
return ARRAYS;
}
public static IotSerializeTypeEnum of(String type) {
return ArrayUtil.firstMatch(e -> e.getType().equals(type), values());
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -24,4 +24,14 @@ public interface IotMessageBus {
*/
void register(IotMessageSubscriber<?> subscriber);
/**
* 取消注册消息订阅者
*
* @param subscriber 订阅者
*/
default void unregister(IotMessageSubscriber<?> subscriber) {
// TODO 芋艿:暂时不实现,需求量不大,但是
// throw new UnsupportedOperationException("取消注册消息订阅者功能,尚未实现");
}
}

View File

@@ -26,4 +26,16 @@ public interface IotMessageSubscriber<T> {
*/
void onMessage(T message);
/**
* 启动订阅
*/
default void start() {
}
/**
* 停止订阅
*/
default void stop() {
}
}

View File

@@ -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;
@@ -60,7 +60,7 @@ public class IotDeviceMessage {
*/
private String serverId;
// ========== codec编解码字段 ==========
// ========== serialize序列化相关字段 ==========
/**
* 请求编号
@@ -72,7 +72,7 @@ public class IotDeviceMessage {
* 请求方法
*
* 枚举 {@link IotDeviceMessageMethodEnum}
* 例如说thing.property.report 属性上报
* 例如说thing.property.post 属性上报
*/
private String method;
/**
@@ -94,7 +94,7 @@ public class IotDeviceMessage {
*/
private String msg;
// ========== 基础方法:只传递"codec编解码字段" ==========
// ========== 基础方法:只传递"serialize序列化相关字段" ==========
public static IotDeviceMessage requestOf(String method) {
return requestOf(null, method, null);
@@ -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()));
}
}

View File

@@ -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>
@@ -27,9 +30,11 @@ public class IotDeviceRegisterReqDTO {
private String deviceName;
/**
* 产品密钥
* 注册签名
*
* @see cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils#buildSign(String, String, String)
*/
@NotEmpty(message = "产品密钥不能为空")
private String productSecret;
@NotEmpty(message = "签名不能为空")
private String sign;
}

View File

@@ -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>

View File

@@ -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 芋道源码

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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>
* 本质是一个 Mapkey 为属性标识符value 为属性值
*

View File

@@ -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>
* 本质是一个 Mapkey 为属性标识符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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.iot.core.util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
/**
* IoT 产品【动态注册】认证工具类
* <p>
* 用于一型一密场景,使用 productSecret 生成签名
*
* @author 芋道源码
*/
public class IotProductAuthUtils {
/**
* 生成设备动态注册签名
*
* @param productKey 产品标识
* @param deviceName 设备名称
* @param productSecret 产品密钥
* @return 签名
*/
public static String buildSign(String productKey, String deviceName, String productSecret) {
String content = buildContent(productKey, deviceName);
return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret))
.digestHex(content);
}
/**
* 验证设备动态注册签名
*
* @param productKey 产品标识
* @param deviceName 设备名称
* @param productSecret 产品密钥
* @param sign 待验证的签名
* @return 是否验证通过
*/
public static boolean verifySign(String productKey, String deviceName, String productSecret, String sign) {
String expectedSign = buildSign(productKey, deviceName, productSecret);
return expectedSign.equals(sign);
}
/**
* 构建签名内容
*
* @param productKey 产品标识
* @param deviceName 设备名称
* @return 签名内容
*/
private static String buildContent(String productKey, String deviceName) {
return "deviceName" + deviceName + "productKey" + productKey;
}
}

View File

@@ -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>

View File

@@ -1,33 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.codec;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
/**
* {@link cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage} 的编解码器
*
* @author 芋道源码
*/
public interface IotDeviceMessageCodec {
/**
* 编码消息
*
* @param message 消息
* @return 编码后的消息内容
*/
byte[] encode(IotDeviceMessage message);
/**
* 解码消息
*
* @param bytes 消息内容
* @return 解码后的消息内容
*/
IotDeviceMessage decode(byte[] bytes);
/**
* @return 数据格式(编码器类型)
*/
String type();
}

View File

@@ -1,89 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.codec.alink;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 阿里云 Alink {@link IotDeviceMessage} 的编解码器
*
* @author 芋道源码
*/
@Component
public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
public static final String TYPE = "Alink";
@Data
@NoArgsConstructor
@AllArgsConstructor
private static class AlinkMessage {
public static final String VERSION_1 = "1.0";
/**
* 消息 ID且每个消息 ID 在当前设备具有唯一性
*/
private String id;
/**
* 版本号
*/
private String version;
/**
* 请求方法
*/
private String method;
/**
* 请求参数
*/
private Object params;
/**
* 响应结果
*/
private Object data;
/**
* 响应错误码
*/
private Integer code;
/**
* 响应提示
*
* 特殊:这里阿里云是 message为了保持和项目的 {@link CommonResult#getMsg()} 一致。
*/
private String msg;
}
@Override
public String type() {
return TYPE;
}
@Override
public byte[] encode(IotDeviceMessage message) {
AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1,
message.getMethod(), message.getParams(), message.getData(), message.getCode(), message.getMsg());
return JsonUtils.toJsonByte(alinkMessage);
}
@Override
@SuppressWarnings("DataFlowIssue")
public IotDeviceMessage decode(byte[] bytes) {
AlinkMessage alinkMessage = JsonUtils.parseObject(bytes, AlinkMessage.class);
Assert.notNull(alinkMessage, "消息不能为空");
Assert.equals(alinkMessage.getVersion(), AlinkMessage.VERSION_1, "消息版本号必须是 1.0");
return IotDeviceMessage.of(alinkMessage.getId(), alinkMessage.getMethod(), alinkMessage.getParams(),
alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg());
}
}

View File

@@ -1,4 +0,0 @@
/**
* 提供设备接入的各种数据(请求、响应)的编解码
*/
package cn.iocoder.yudao.module.iot.gateway.codec;

View File

@@ -1,4 +0,0 @@
/**
* TODO @芋艿:实现一个 alink 的 xml 版本
*/
package cn.iocoder.yudao.module.iot.gateway.codec.simple;

View File

@@ -1,110 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
/**
* TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器
*
* 采用纯 JSON 格式传输,格式如下:
* {
* "id": "消息 ID",
* "method": "消息方法",
* "params": {...}, // 请求参数
* "data": {...}, // 响应结果
* "code": 200, // 响应错误码
* "msg": "success", // 响应提示
* "timestamp": 时间戳
* }
*
* @author 芋道源码
*/
@Component
public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec {
public static final String TYPE = "TCP_JSON";
@Data
@NoArgsConstructor
@AllArgsConstructor
private static class TcpJsonMessage {
/**
* 消息 ID且每个消息 ID 在当前设备具有唯一性
*/
private String id;
/**
* 请求方法
*/
private String method;
/**
* 请求参数
*/
private Object params;
/**
* 响应结果
*/
private Object data;
/**
* 响应错误码
*/
private Integer code;
/**
* 响应提示
*/
private String msg;
/**
* 时间戳
*/
private Long timestamp;
}
@Override
public String type() {
return TYPE;
}
@Override
public byte[] encode(IotDeviceMessage message) {
TcpJsonMessage tcpJsonMessage = new TcpJsonMessage(
message.getRequestId(),
message.getMethod(),
message.getParams(),
message.getData(),
message.getCode(),
message.getMsg(),
System.currentTimeMillis());
return JsonUtils.toJsonByte(tcpJsonMessage);
}
@Override
@SuppressWarnings("DataFlowIssue")
public IotDeviceMessage decode(byte[] bytes) {
String jsonStr = StrUtil.utf8Str(bytes).trim();
TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(jsonStr, TcpJsonMessage.class);
Assert.notNull(tcpJsonMessage, "消息不能为空");
Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空");
return IotDeviceMessage.of(
tcpJsonMessage.getId(),
tcpJsonMessage.getMethod(),
tcpJsonMessage.getParams(),
tcpJsonMessage.getData(),
tcpJsonMessage.getCode(),
tcpJsonMessage.getMsg());
}
}

View File

@@ -1,254 +1,28 @@
package cn.iocoder.yudao.module.iot.gateway.config;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager;
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
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)
@Slf4j
public class IotGatewayConfiguration {
/**
* IoT 网关 HTTP 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.http", name = "enabled", havingValue = "true")
@Slf4j
public static class HttpProtocolConfiguration {
@Bean(name = "httpVertx", destroyMethod = "close")
public Vertx httpVertx() {
return Vertx.vertx();
}
@Bean
public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties,
@Qualifier("httpVertx") Vertx httpVertx) {
return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), httpVertx);
}
@Bean
public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol,
IotMessageBus messageBus) {
return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus);
}
@Bean
public IotMessageSerializerManager iotMessageSerializerManager() {
return new IotMessageSerializerManager();
}
/**
* IoT 网关 EMQX 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true")
@Slf4j
public static class EmqxProtocolConfiguration {
@Bean(name = "emqxVertx", destroyMethod = "close")
public Vertx emqxVertx() {
return Vertx.vertx();
}
@Bean
public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties,
@Qualifier("emqxVertx") Vertx emqxVertx) {
return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
}
@Bean
public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties,
@Qualifier("emqxVertx") Vertx emqxVertx) {
return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
}
@Bean
public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol,
IotMessageBus messageBus) {
return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus);
}
}
/**
* IoT 网关 TCP 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.tcp", name = "enabled", havingValue = "true")
@Slf4j
public static class TcpProtocolConfiguration {
@Bean(name = "tcpVertx", destroyMethod = "close")
public Vertx tcpVertx() {
return Vertx.vertx();
}
@Bean
public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotTcpConnectionManager connectionManager,
@Qualifier("tcpVertx") Vertx tcpVertx) {
return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(),
deviceService, messageService, connectionManager, tcpVertx);
}
@Bean
public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
IotTcpConnectionManager connectionManager,
IotMessageBus messageBus) {
return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
}
}
/**
* IoT 网关 MQTT 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true")
@Slf4j
public static class MqttProtocolConfiguration {
@Bean(name = "mqttVertx", destroyMethod = "close")
public Vertx mqttVertx() {
return Vertx.vertx();
}
@Bean
public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceMessageService messageService,
IotMqttConnectionManager connectionManager,
@Qualifier("mqttVertx") Vertx mqttVertx) {
return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService,
connectionManager, mqttVertx);
}
@Bean
public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService,
IotMqttConnectionManager connectionManager) {
return new IotMqttDownstreamHandler(messageService, connectionManager);
}
@Bean
public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol,
IotMqttDownstreamHandler downstreamHandler,
IotMessageBus messageBus) {
return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus);
}
}
/**
* IoT 网关 UDP 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true")
@Slf4j
public static class UdpProtocolConfiguration {
@Bean(name = "udpVertx", destroyMethod = "close")
public Vertx udpVertx() {
return Vertx.vertx();
}
@Bean
public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotUdpSessionManager sessionManager,
@Qualifier("udpVertx") Vertx udpVertx) {
return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(),
deviceService, messageService, sessionManager, udpVertx);
}
@Bean
public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
IotUdpSessionManager sessionManager,
IotMessageBus messageBus) {
return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus);
}
}
/**
* IoT 网关 CoAP 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true")
@Slf4j
public static class CoapProtocolConfiguration {
@Bean
public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) {
return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap());
}
@Bean
public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol,
IotMessageBus messageBus) {
return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus);
}
}
/**
* IoT 网关 WebSocket 协议配置类
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true")
@Slf4j
public static class WebSocketProtocolConfiguration {
@Bean(name = "websocketVertx", destroyMethod = "close")
public Vertx websocketVertx() {
return Vertx.vertx();
}
@Bean
public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotWebSocketConnectionManager connectionManager,
@Qualifier("websocketVertx") Vertx websocketVertx) {
return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(),
deviceService, messageService, connectionManager, websocketVertx);
}
@Bean
public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
IotWebSocketConnectionManager connectionManager,
IotMessageBus messageBus) {
return new IotWebSocketDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
}
@Bean
public IotProtocolManager iotProtocolManager(IotGatewayProperties gatewayProperties) {
return new IotProtocolManager(gatewayProperties);
}
}

View File

@@ -1,5 +1,16 @@
package cn.iocoder.yudao.module.iot.gateway.config;
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;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -24,9 +35,9 @@ public class IotGatewayProperties {
private TokenProperties token;
/**
* 协议配置
* 协议实例列表
*/
private ProtocolProperties protocol;
private List<ProtocolProperties> protocols;
@Data
public static class RpcProperties {
@@ -65,582 +76,158 @@ public class IotGatewayProperties {
}
/**
* 协议实例配置
*/
@Data
public static class ProtocolProperties {
/**
* HTTP 组件配置
* 协议实例 ID如 "http-alink"、"tcp-binary"
*/
private HttpProperties http;
@NotEmpty(message = "协议实例 ID 不能为空")
private String id;
/**
* EMQX 组件配置
* 是否启用
*/
private EmqxProperties emqx;
@NotNull(message = "是否启用不能为空")
private Boolean enabled = true;
/**
* TCP 组件配置
* 协议类型
*
* @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum
*/
private TcpProperties tcp;
/**
* MQTT 组件配置
*/
private MqttProperties mqtt;
/**
* MQTT WebSocket 组件配置
*/
private MqttWsProperties mqttWs;
/**
* UDP 组件配置
*/
private UdpProperties udp;
/**
* CoAP 组件配置
*/
private CoapProperties coap;
/**
* WebSocket 组件配置
*/
private WebSocketProperties websocket;
}
@Data
public static class HttpProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
@NotEmpty(message = "协议类型不能为空")
private String protocol;
/**
* 服务端口
*/
private Integer serverPort;
/**
* 是否开启 SSL
*/
@NotNull(message = "是否开启 SSL 不能为空")
private Boolean sslEnabled = false;
/**
* SSL 证书路径
*/
private String sslKeyPath;
/**
* SSL 证书路径
*/
private String sslCertPath;
}
@Data
public static class EmqxProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* HTTP 服务端口默认8090
*/
private Integer httpPort = 8090;
/**
* MQTT 服务器地址
*/
@NotEmpty(message = "MQTT 服务器地址不能为空")
private String mqttHost;
/**
* MQTT 服务器端口默认1883
*/
@NotNull(message = "MQTT 服务器端口不能为空")
private Integer mqttPort = 1883;
/**
* MQTT 用户名
*/
@NotEmpty(message = "MQTT 用户名不能为空")
private String mqttUsername;
/**
* MQTT 密码
*/
@NotEmpty(message = "MQTT 密码不能为空")
private String mqttPassword;
/**
* MQTT 客户端的 SSL 开关
*/
@NotNull(message = "MQTT 是否开启 SSL 不能为空")
private Boolean mqttSsl = false;
/**
* MQTT 客户端 ID如果为空系统将自动生成
*/
@NotEmpty(message = "MQTT 客户端 ID 不能为空")
private String mqttClientId;
/**
* MQTT 订阅的主题
*/
@NotEmpty(message = "MQTT 主题不能为空")
private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
/**
* 默认 QoS 级别
* <p>
* 0 - 最多一次
* 1 - 至少一次
* 2 - 刚好一次
* 不同协议含义不同:
* 1. TCP/UDP/HTTP/WebSocket/MQTT/CoAP对应网关自身监听的服务端口
* 2. EMQX对应网关提供给 EMQX 回调的 HTTP Hook 端口(/mqtt/auth、/mqtt/acl、/mqtt/event
*/
private Integer mqttQos = 1;
@NotNull(message = "服务端口不能为空")
private Integer port;
/**
* 序列化类型(可选)
*
* @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum
*
* 为什么是可选的呢?
* 1. {@link IotProtocolTypeEnum#HTTP}、{@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式
* 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析
*/
private String serialize;
// ========== SSL 配置 ==========
/**
* 连接超时时间(秒
* SSL 配置(可选,配置文件中不配置则为 null
*/
private Integer connectTimeoutSeconds = 10;
@Valid
private SslConfig ssl;
// ========== 各协议配置 ==========
/**
* 重连延迟时间(毫秒)
* HTTP 协议配置
*/
private Long reconnectDelayMs = 5000L;
@Valid
private IotHttpConfig http;
/**
* WebSocket 协议配置
*/
@Valid
private IotWebSocketConfig websocket;
/**
* 是否启用 Clean Session (清理会话)
* true: 每次连接都是新会话Broker 不保留离线消息和订阅关系。
* 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。
* TCP 协议配置
*/
private Boolean cleanSession = true;
@Valid
private IotTcpConfig tcp;
/**
* UDP 协议配置
*/
@Valid
private IotUdpConfig udp;
/**
* 心跳间隔(秒)
* 用于保持连接活性,及时发现网络中断。
* CoAP 协议配置
*/
private Integer keepAliveIntervalSeconds = 60;
@Valid
private IotCoapConfig coap;
/**
* 最大未确认消息队列大小
* 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。
* MQTT 协议配置
*/
private Integer maxInflightQueue = 10000;
@Valid
private IotMqttConfig mqtt;
/**
* EMQX 协议配置
*/
@Valid
private IotEmqxConfig emqx;
/**
* 是否信任所有 SSL 证书
* 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用!
* 在生产环境中,应设置为 false并配置正确的信任库。
* Modbus TCP Client 协议配置
*/
private Boolean trustAll = false;
@Valid
private IotModbusTcpClientConfig modbusTcpClient;
/**
* 遗嘱消息配置 (用于网关异常下线时通知其他系统)
* Modbus TCP Server 协议配置
*/
private final Will will = new Will();
/**
* 高级 SSL/TLS 配置 (用于生产环境)
*/
private final Ssl sslOptions = new Ssl();
/**
* 遗嘱消息 (Last Will and Testament)
*/
@Data
public static class Will {
/**
* 是否启用遗嘱消息
*/
private boolean enabled = false;
/**
* 遗嘱消息主题
*/
private String topic;
/**
* 遗嘱消息内容
*/
private String payload;
/**
* 遗嘱消息 QoS 等级
*/
private Integer qos = 1;
/**
* 遗嘱消息是否作为保留消息发布
*/
private boolean retain = true;
}
/**
* 高级 SSL/TLS 配置
*/
@Data
public static class Ssl {
/**
* 密钥库KeyStore路径例如classpath:certs/client.jks
* 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。
*/
private String keyStorePath;
/**
* 密钥库密码
*/
private String keyStorePassword;
/**
* 信任库TrustStore路径例如classpath:certs/trust.jks
* 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。
*/
private String trustStorePath;
/**
* 信任库密码
*/
private String trustStorePassword;
}
@Valid
private IotModbusTcpServerConfig modbusTcpServer;
}
/**
* SSL 配置
*/
@Data
public static class TcpProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 服务器端口
*/
private Integer port = 8091;
/**
* 心跳超时时间(毫秒)
*/
private Long keepAliveTimeoutMs = 30000L;
/**
* 最大连接数
*/
private Integer maxConnections = 1000;
/**
* 是否启用SSL
*/
private Boolean sslEnabled = false;
/**
* SSL证书路径
*/
private String sslCertPath;
/**
* SSL私钥路径
*/
private String sslKeyPath;
}
@Data
public static class MqttProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 服务器端口
*/
private Integer port = 1883;
/**
* 最大消息大小(字节)
*/
private Integer maxMessageSize = 8192;
/**
* 连接超时时间(秒)
*/
private Integer connectTimeoutSeconds = 60;
/**
* 保持连接超时时间(秒)
*/
private Integer keepAliveTimeoutSeconds = 300;
public static class SslConfig {
/**
* 是否启用 SSL
*/
private Boolean sslEnabled = false;
/**
* SSL 配置
*/
private SslOptions sslOptions = new SslOptions();
/**
* SSL 配置选项
*/
@Data
public static class SslOptions {
/**
* 密钥证书选项
*/
private io.vertx.core.net.KeyCertOptions keyCertOptions;
/**
* 信任选项
*/
private io.vertx.core.net.TrustOptions trustOptions;
/**
* SSL 证书路径
*/
private String certPath;
/**
* SSL 私钥路径
*/
private String keyPath;
/**
* 信任存储路径
*/
private String trustStorePath;
/**
* 信任存储密码
*/
private String trustStorePassword;
}
}
@Data
public static class MqttWsProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* WebSocket 服务器端口默认8083
*/
private Integer port = 8083;
/**
* WebSocket 路径(默认:/mqtt
*/
@NotEmpty(message = "WebSocket 路径不能为空")
private String path = "/mqtt";
/**
* 最大消息大小(字节)
*/
private Integer maxMessageSize = 8192;
/**
* 连接超时时间(秒)
*/
private Integer connectTimeoutSeconds = 60;
/**
* 保持连接超时时间(秒)
*/
private Integer keepAliveTimeoutSeconds = 300;
/**
* 是否启用 SSLwss://
*/
private Boolean sslEnabled = false;
/**
* SSL 配置
*/
private SslOptions sslOptions = new SslOptions();
/**
* WebSocket 子协议(通常为 "mqtt" 或 "mqttv3.1"
*/
@NotEmpty(message = "WebSocket 子协议不能为空")
private String subProtocol = "mqtt";
/**
* 最大帧大小(字节)
*/
private Integer maxFrameSize = 65536;
/**
* SSL 配置选项
*/
@Data
public static class SslOptions {
/**
* 密钥证书选项
*/
private io.vertx.core.net.KeyCertOptions keyCertOptions;
/**
* 信任选项
*/
private io.vertx.core.net.TrustOptions trustOptions;
/**
* SSL 证书路径
*/
private String certPath;
/**
* SSL 私钥路径
*/
private String keyPath;
/**
* 信任存储路径
*/
private String trustStorePath;
/**
* 信任存储密码
*/
private String trustStorePassword;
}
}
@Data
public static class UdpProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 服务端口(默认 8093
*/
private Integer port = 8093;
/**
* 接收缓冲区大小(默认 64KB
*/
private Integer receiveBufferSize = 65536;
/**
* 发送缓冲区大小(默认 64KB
*/
private Integer sendBufferSize = 65536;
/**
* 会话超时时间(毫秒,默认 60 秒)
* <p>
* 用于清理不活跃的设备地址映射
*/
private Long sessionTimeoutMs = 60000L;
/**
* 会话清理间隔(毫秒,默认 30 秒)
*/
private Long sessionCleanIntervalMs = 30000L;
}
@Data
public static class CoapProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 服务端口CoAP 默认端口 5683
*/
@NotNull(message = "服务端口不能为空")
private Integer port = 5683;
/**
* 最大消息大小(字节)
*/
@NotNull(message = "最大消息大小不能为空")
private Integer maxMessageSize = 1024;
/**
* ACK 超时时间(毫秒)
*/
@NotNull(message = "ACK 超时时间不能为空")
private Integer ackTimeout = 2000;
/**
* 最大重传次数
*/
@NotNull(message = "最大重传次数不能为空")
private Integer maxRetransmit = 4;
}
@Data
public static class WebSocketProperties {
/**
* 是否开启
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 服务器端口默认8094
*/
private Integer port = 8094;
/**
* WebSocket 路径(默认:/ws
*/
@NotEmpty(message = "WebSocket 路径不能为空")
private String path = "/ws";
/**
* 最大消息大小(字节,默认 64KB
*/
private Integer maxMessageSize = 65536;
/**
* 最大帧大小(字节,默认 64KB
*/
private Integer maxFrameSize = 65536;
/**
* 空闲超时时间(秒,默认 60
*/
private Integer idleTimeoutSeconds = 60;
/**
* 是否启用 SSLwss://
*/
private Boolean sslEnabled = false;
@NotNull(message = "是否启用 SSL 不能为空")
private Boolean ssl = false;
/**
* SSL 证书路径
*/
@NotEmpty(message = "SSL 证书路径不能为空")
private String sslCertPath;
/**
* SSL 私钥路径
*/
@NotEmpty(message = "SSL 私钥路径不能为空")
private String sslKeyPath;
/**
* 密钥库KeyStore路径
* <p>
* 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)
*/
private String keyStorePath;
/**
* 密钥库密码
*/
private String keyStorePassword;
/**
* 信任库TrustStore路径
* <p>
* 包含服务端信任的 CA 证书,用于验证服务端的身份
*/
private String trustStorePath;
/**
* 信任库密码
*/
private String trustStorePassword;
}
}

View File

@@ -1,49 +1,53 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
package cn.iocoder.yudao.module.iot.gateway.protocol;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler;
import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 EMQX 订阅者接收下行给设备的消息
* IoT 协议下行消息订阅者抽象类
*
* 负责接收来自消息总线的下行消息并委托给子类进行业务处理
*
* @author 芋道源码
*/
@AllArgsConstructor
@Slf4j
public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
public abstract class AbstractIotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotEmqxDownstreamHandler downstreamHandler;
private final IotProtocol protocol;
private final IotMessageBus messageBus;
private final IotEmqxUpstreamProtocol protocol;
public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) {
this.protocol = protocol;
this.messageBus = messageBus;
this.downstreamHandler = new IotEmqxDownstreamHandler(protocol);
}
@PostConstruct
public void init() {
messageBus.register(this);
}
@Override
public String getTopic() {
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
}
/**
* 保证点对点消费需要保证独立的 Group所以使用 Topic 作为 Group
*/
@Override
public String getGroup() {
// 保证点对点消费需要保证独立的 Group所以使用 Topic 作为 Group
return getTopic();
}
@Override
public void start() {
messageBus.register(this);
log.info("[start][{} 下行消息订阅成功Topic{}]", protocol.getType().name(), getTopic());
}
@Override
public void stop() {
messageBus.unregister(this);
log.info("[stop][{} 下行消息订阅已停止Topic{}]", protocol.getType().name(), getTopic());
}
@Override
public void onMessage(IotDeviceMessage message) {
log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]",
@@ -51,18 +55,25 @@ public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber<IotDevi
try {
// 1. 校验
String method = message.getMethod();
if (method == null) {
if (StrUtil.isBlank(method)) {
log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]",
message.getId(), message.getDeviceId());
return;
}
// 2. 处理下行消息
downstreamHandler.handle(message);
handleMessage(message);
} catch (Exception e) {
log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]",
message.getId(), message.getMethod(), message.getDeviceId(), e);
}
}
}
/**
* 处理下行消息
*
* @param message 下行消息
*/
protected abstract void handleMessage(IotDeviceMessage message);
}

View File

@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.iot.gateway.protocol;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
/**
* IoT 协议接口
*
* 定义传输层协议的生命周期管理
*
* @author 芋道源码
*/
public interface IotProtocol {
/**
* 获取协议实例 ID
*
* @return 协议实例 ID如 "http-alink"、"tcp-binary"
*/
String getId();
/**
* 获取服务器 ID用于消息追踪全局唯一
*
* @return 服务器 ID
*/
String getServerId();
/**
* 获取协议类型
*
* @return 协议类型枚举
*/
IotProtocolTypeEnum getType();
/**
* 启动协议服务
*/
void start();
/**
* 停止协议服务
*/
void stop();
/**
* 检查协议服务是否正在运行
*
* @return 是否正在运行
*/
boolean isRunning();
}

View File

@@ -0,0 +1,217 @@
package cn.iocoder.yudao.module.iot.gateway.protocol;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
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;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.SmartLifecycle;
import java.util.ArrayList;
import java.util.List;
/**
* IoT 协议管理器:负责根据配置创建和管理协议实例
*
* @author 芋道源码
*/
@Slf4j
public class IotProtocolManager implements SmartLifecycle {
private final IotGatewayProperties gatewayProperties;
/**
* 协议实例列表
*/
private final List<IotProtocol> protocols = new ArrayList<>();
@Getter
private volatile boolean running = false;
public IotProtocolManager(IotGatewayProperties gatewayProperties) {
this.gatewayProperties = gatewayProperties;
}
@Override
public void start() {
if (running) {
return;
}
List<IotGatewayProperties.ProtocolProperties> protocolConfigs = gatewayProperties.getProtocols();
if (CollUtil.isEmpty(protocolConfigs)) {
log.info("[start][没有配置协议实例,跳过启动]");
return;
}
for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) {
if (BooleanUtil.isFalse(config.getEnabled())) {
log.info("[start][协议实例 {} 未启用,跳过]", config.getId());
continue;
}
IotProtocol protocol = createProtocol(config);
if (protocol == null) {
continue;
}
protocol.start();
protocols.add(protocol);
}
running = true;
log.info("[start][协议管理器启动完成,共启动 {} 个协议实例]", protocols.size());
}
@Override
public void stop() {
if (!running) {
return;
}
for (IotProtocol protocol : protocols) {
try {
protocol.stop();
} catch (Exception e) {
log.error("[stop][协议实例 {} 停止失败]", protocol.getId(), e);
}
}
protocols.clear();
running = false;
log.info("[stop][协议管理器已停止]");
}
/**
* 创建协议实例
*
* @param config 协议实例配置
* @return 协议实例
*/
@SuppressWarnings({"EnhancedSwitchMigration"})
private IotProtocol createProtocol(IotGatewayProperties.ProtocolProperties config) {
IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol());
if (protocolType == null) {
log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol());
return null;
}
switch (protocolType) {
case HTTP:
return createHttpProtocol(config);
case TCP:
return createTcpProtocol(config);
case UDP:
return createUdpProtocol(config);
case COAP:
return createCoapProtocol(config);
case WEBSOCKET:
return createWebSocketProtocol(config);
case MQTT:
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));
}
}
/**
* 创建 HTTP 协议实例
*
* @param config 协议实例配置
* @return HTTP 协议实例
*/
private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotHttpProtocol(config);
}
/**
* 创建 TCP 协议实例
*
* @param config 协议实例配置
* @return TCP 协议实例
*/
private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotTcpProtocol(config);
}
/**
* 创建 UDP 协议实例
*
* @param config 协议实例配置
* @return UDP 协议实例
*/
private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotUdpProtocol(config);
}
/**
* 创建 CoAP 协议实例
*
* @param config 协议实例配置
* @return CoAP 协议实例
*/
private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotCoapProtocol(config);
}
/**
* 创建 WebSocket 协议实例
*
* @param config 协议实例配置
* @return WebSocket 协议实例
*/
private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotWebSocketProtocol(config);
}
/**
* 创建 MQTT 协议实例
*
* @param config 协议实例配置
* @return MQTT 协议实例
*/
private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotMqttProtocol(config);
}
/**
* 创建 EMQX 协议实例
*
* @param config 协议实例配置
* @return EMQX 协议实例
*/
private IotEmqxProtocol createEmqxProtocol(IotGatewayProperties.ProtocolProperties config) {
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);
}
}

View File

@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* IoT CoAP 协议配置
*
* @author 芋道源码
*/
@Data
public class IotCoapConfig {
/**
* 最大消息大小(字节)
*/
@NotNull(message = "最大消息大小不能为空")
@Min(value = 64, message = "最大消息大小必须大于 64 字节")
private Integer maxMessageSize = 1024;
/**
* ACK 超时时间(毫秒)
*/
@NotNull(message = "ACK 超时时间不能为空")
@Min(value = 100, message = "ACK 超时时间必须大于 100 毫秒")
private Integer ackTimeoutMs = 2000;
/**
* 最大重传次数
*/
@NotNull(message = "最大重传次数不能为空")
@Min(value = 0, message = "最大重传次数必须大于等于 0")
private Integer maxRetransmit = 4;
}

View File

@@ -1,46 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 CoAP 订阅者:接收下行给设备的消息
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotCoapDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotCoapUpstreamProtocol protocol;
private final IotMessageBus messageBus;
@PostConstruct
public void init() {
messageBus.register(this);
}
@Override
public String getTopic() {
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
}
@Override
public String getGroup() {
// 保证点对点消费,需要保证独立的 Group所以使用 Topic 作为 Group
return getTopic();
}
@Override
public void onMessage(IotDeviceMessage message) {
// 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更)
log.warn("[onMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message);
}
}

View File

@@ -0,0 +1,175 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import cn.hutool.extra.spring.SpringUtil;
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.coap.handler.downstream.IotCoapDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthResource;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterResource;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterSubHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapRegisterSubResource;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapUpstreamTopicResource;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import cn.hutool.core.lang.Assert;
import java.util.concurrent.TimeUnit;
/**
* IoT CoAP 协议实现
* <p>
* 基于 Eclipse Californium 实现,支持:
* 1. 认证POST /auth
* 2. 设备动态注册POST /auth/register/device
* 3. 子设备动态注册POST /auth/register/sub-device/{productKey}/{deviceName}
* 4. 属性上报POST /topic/sys/{productKey}/{deviceName}/thing/property/post
* 5. 事件上报POST /topic/sys/{productKey}/{deviceName}/thing/event/post
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapProtocol implements IotProtocol {
/**
* 协议配置
*/
private final ProtocolProperties properties;
/**
* 服务器 ID用于消息追踪全局唯一
*/
@Getter
private final String serverId;
/**
* 运行状态
*/
@Getter
private volatile boolean running = false;
/**
* CoAP 服务器
*/
private CoapServer coapServer;
/**
* 下行消息订阅者
*/
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());
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.COAP;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT CoAP 协议 {} 已经在运行中]", getId());
return;
}
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());
config.set(CoapConfig.ACK_TIMEOUT, coapConfig.getAckTimeoutMs(), TimeUnit.MILLISECONDS);
config.set(CoapConfig.MAX_RETRANSMIT, coapConfig.getMaxRetransmit());
// 1.2 创建 CoAP 服务器
coapServer = new CoapServer(config);
// 2.1 添加 /auth 认证资源
IotCoapAuthHandler authHandler = new IotCoapAuthHandler(serverId);
IotCoapAuthResource authResource = new IotCoapAuthResource(authHandler);
coapServer.add(authResource);
// 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
// 2.3 添加 /auth/register/sub-device/{productKey}/{deviceName} 子设备动态注册资源
IotCoapRegisterSubHandler registerSubHandler = new IotCoapRegisterSubHandler();
IotCoapRegisterSubResource registerSubResource = new IotCoapRegisterSubResource(registerSubHandler);
authResource.add(new CoapResource("register") {{
add(registerResource);
add(registerSubResource);
}});
// 2.4 添加 /topic 根资源(用于上行消息)
IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(serverId);
IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(serverId, upstreamHandler);
coapServer.add(topicResource);
// 3. 启动服务器
coapServer.start();
running = true;
log.info("[start][IoT CoAP 协议 {} 启动成功,端口:{}serverId{}]",
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);
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 CoAP 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2. 关闭 CoAP 服务器
if (coapServer != null) {
try {
coapServer.stop();
coapServer.destroy();
coapServer = null;
log.info("[stop][IoT CoAP 协议 {} 服务器已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT CoAP 协议 {} 服务器停止失败]", getId(), e);
}
}
running = false;
log.info("[stop][IoT CoAP 协议 {} 已停止]", getId());
}
}

View File

@@ -1,95 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthResource;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterResource;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamTopicResource;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamHandler;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import java.util.concurrent.TimeUnit;
/**
* IoT 网关 CoAP 协议:接收设备上行消息
*
* 基于 Eclipse Californium 实现,支持:
* 1. 认证POST /auth
* 2. 属性上报POST /topic/sys/{productKey}/{deviceName}/thing/property/post
* 3. 事件上报POST /topic/sys/{productKey}/{deviceName}/thing/event/post
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapUpstreamProtocol {
private final IotGatewayProperties.CoapProperties coapProperties;
private CoapServer coapServer;
@Getter
private final String serverId;
public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) {
this.coapProperties = coapProperties;
this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort());
}
@PostConstruct
public void start() {
try {
// 1.1 创建网络配置Californium 3.x API
Configuration config = Configuration.createStandardWithoutFile();
config.set(CoapConfig.COAP_PORT, coapProperties.getPort());
config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize());
config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS);
config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit());
// 1.2 创建 CoAP 服务器
coapServer = new CoapServer(config);
// 2.1 添加 /auth 认证资源
IotCoapAuthHandler authHandler = new IotCoapAuthHandler();
IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler);
coapServer.add(authResource);
// 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
authResource.add(new CoapResource("register") {{
add(registerResource);
}});
// 2.3 添加 /topic 根资源(用于上行消息)
IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler();
IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler);
coapServer.add(topicResource);
// 3. 启动服务器
coapServer.start();
log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort());
} catch (Exception e) {
log.error("[start][IoT 网关 CoAP 协议启动失败]", e);
throw e;
}
}
@PreDestroy
public void stop() {
if (coapServer != null) {
try {
coapServer.stop();
log.info("[stop][IoT 网关 CoAP 协议已停止]");
} catch (Exception e) {
log.error("[stop][IoT 网关 CoAP 协议停止失败]", e);
}
}
}
}

View File

@@ -0,0 +1,27 @@
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.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT 网关 CoAP 订阅者:接收下行给设备的消息
*
* @author 芋道源码
*/
@Slf4j
public class IotCoapDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) {
super(protocol, messageBus);
}
@Override
protected void handleMessage(IotDeviceMessage message) {
// 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更)
log.warn("[handleMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message);
}
}

View File

@@ -0,0 +1,186 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.coap.Option;
import org.eclipse.californium.core.server.resources.CoapExchange;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* IoT 网关 CoAP 协议的处理器抽象基类:提供通用的前置处理(认证)、请求解析、响应处理、全局的异常捕获等
*
* @author 芋道源码
*/
@Slf4j
public abstract class IotCoapAbstractHandler {
/**
* 自定义 CoAP Option 编号,用于携带 Token
* <p>
* CoAP Option 范围 2048-65535 属于实验/自定义范围
*/
public static final int OPTION_TOKEN = 2088;
private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
/**
* 处理 CoAP 请求(模板方法)
*
* @param exchange CoAP 交换对象
*/
public final void handle(CoapExchange exchange) {
try {
// 1. 前置处理
beforeHandle(exchange);
// 2. 执行业务逻辑
CommonResult<Object> result = handle0(exchange);
writeResponse(exchange, result);
} catch (ServiceException e) {
// 业务异常,返回对应的错误码和消息
writeResponse(exchange, CommonResult.error(e.getCode(), e.getMessage()));
} catch (IllegalArgumentException e) {
// 参数校验异常hutool Assert 抛出),返回 BAD_REQUEST
writeResponse(exchange, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage()));
} catch (Exception e) {
// 其他未知异常,返回 INTERNAL_SERVER_ERROR
log.error("[handle][CoAP 请求处理异常]", e);
writeResponse(exchange, CommonResult.error(INTERNAL_SERVER_ERROR));
}
}
/**
* 处理 CoAP 请求(子类实现)
*
* @param exchange CoAP 交换对象
* @return 处理结果
*/
protected abstract CommonResult<Object> handle0(CoapExchange exchange);
/**
* 前置处理:认证等
*
* @param exchange CoAP 交换对象
*/
private void beforeHandle(CoapExchange exchange) {
// 1.1 如果不需要认证,则不走前置处理
if (!requiresAuthentication()) {
return;
}
// 1.2 从自定义 Option 获取 token
String token = getTokenFromOption(exchange);
if (StrUtil.isEmpty(token)) {
throw exception(UNAUTHORIZED);
}
// 1.3 校验 token
IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
if (deviceInfo == null) {
throw exception(UNAUTHORIZED);
}
// 2.1 解析 productKey 和 deviceName
List<String> uriPath = exchange.getRequestOptions().getUriPath();
String productKey = getProductKey(uriPath);
String deviceName = getDeviceName(uriPath);
if (StrUtil.isEmpty(productKey) || StrUtil.isEmpty(deviceName)) {
throw exception(BAD_REQUEST);
}
// 2.2 校验设备信息是否匹配
if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
|| ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
throw exception(FORBIDDEN);
}
}
// ========== Token 相关方法 ==========
/**
* 是否需要认证(子类可覆盖)
* <p>
* 默认不需要认证
*
* @return 是否需要认证
*/
protected boolean requiresAuthentication() {
return false;
}
/**
* 从 URI 路径中获取 productKey子类实现
* <p>
* 默认抛出异常,需要认证的子类必须实现此方法
*
* @param uriPath URI 路径
* @return productKey
*/
protected String getProductKey(List<String> uriPath) {
throw new UnsupportedOperationException("子类需要实现 getProductKey 方法");
}
/**
* 从 URI 路径中获取 deviceName子类实现
* <p>
* 默认抛出异常,需要认证的子类必须实现此方法
*
* @param uriPath URI 路径
* @return deviceName
*/
protected String getDeviceName(List<String> uriPath) {
throw new UnsupportedOperationException("子类需要实现 getDeviceName 方法");
}
/**
* 从自定义 CoAP Option 中获取 Token
*
* @param exchange CoAP 交换对象
* @return Token 值,如果不存在则返回 null
*/
protected String getTokenFromOption(CoapExchange exchange) {
Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(),
o -> o.getNumber() == OPTION_TOKEN);
return option != null ? new String(option.getValue()) : null;
}
// ========== 序列化相关方法 ==========
/**
* 解析请求体为指定类型
*
* @param exchange CoAP 交换对象
* @param clazz 目标类型
* @param <T> 目标类型泛型
* @return 解析后的对象,解析失败返回 null
*/
protected <T> T deserializeRequest(CoapExchange exchange, Class<T> clazz) {
byte[] payload = exchange.getRequestPayload();
if (ArrayUtil.isEmpty(payload)) {
return null;
}
return JsonUtils.parseObject(payload, clazz);
}
private static String serializeResponse(Object data) {
return JsonUtils.toJsonString(data);
}
protected void writeResponse(CoapExchange exchange, CommonResult<?> data) {
String json = serializeResponse(data);
exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);
}
}

Some files were not shown because too many files have changed in this diff Show More