!1498 feat:【iot】modbus-tcp 协议接入:100%

Merge pull request !1498 from 芋道源码/feature/iot-modbus
This commit is contained in:
芋道源码
2026-02-14 03:08:01 +00:00
committed by Gitee
133 changed files with 7300 additions and 272 deletions

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")
@@ -63,6 +79,52 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
}));
}
@Override
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/config-list")
@PermitAll
@TenantIgnore
public CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList(
@RequestBody IotModbusDeviceConfigListReqDTO listReqDTO) {
// 1. 获取 Modbus 连接配置
List<IotDeviceModbusConfigDO> configList = modbusConfigService.getDeviceModbusConfigList(listReqDTO);
if (CollUtil.isEmpty(configList)) {
return success(new ArrayList<>());
}
// 2. 组装返回结果
Set<Long> deviceIds = convertSet(configList, IotDeviceModbusConfigDO::getDeviceId);
Map<Long, IotDeviceDO> deviceMap = deviceService.getDeviceMap(deviceIds);
Map<Long, List<IotDeviceModbusPointDO>> pointMap = modbusPointService.getEnabledDeviceModbusPointMapByDeviceIds(deviceIds);
Map<Long, IotProductDO> productMap = productService.getProductMap(convertSet(deviceMap.values(), IotDeviceDO::getProductId));
List<IotModbusDeviceConfigRespDTO> result = new ArrayList<>(configList.size());
for (IotDeviceModbusConfigDO config : configList) {
// 3.1 获取设备信息
IotDeviceDO device = deviceMap.get(config.getDeviceId());
if (device == null) {
continue;
}
// 3.2 按 protocolType 筛选(如果非空)
if (StrUtil.isNotEmpty(listReqDTO.getProtocolType())) {
IotProductDO product = productMap.get(device.getProductId());
if (product == null || ObjUtil.notEqual(listReqDTO.getProtocolType(), product.getProtocolType())) {
continue;
}
}
// 3.3 获取启用的点位列表
List<IotDeviceModbusPointDO> pointList = pointMap.get(config.getDeviceId());
if (CollUtil.isEmpty(pointList)) {
continue;
}
// 3.4 构建 IotModbusDeviceConfigRespDTO 对象
IotModbusDeviceConfigRespDTO configDTO = BeanUtils.toBean(config, IotModbusDeviceConfigRespDTO.class, o ->
o.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())
.setPoints(BeanUtils.toBean(pointList, IotModbusPointRespDTO.class)));
result.add(configDTO);
}
return success(result);
}
@Override
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register")
@PermitAll

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

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

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

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

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

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

@@ -25,7 +25,8 @@ public enum IotProtocolTypeEnum implements ArrayValuable<String> {
MQTT("mqtt"),
EMQX("emqx"),
COAP("coap"),
MODBUS_TCP("modbus_tcp");
MODBUS_TCP_CLIENT("modbus_tcp_client"),
MODBUS_TCP_SERVER("modbus_tcp_server");
public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new);

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

@@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.iot.core.mq.message;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
import cn.iocoder.yudao.module.iot.core.topic.state.IotDeviceStateUpdateReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -72,7 +72,7 @@ public class IotDeviceMessage {
* 请求方法
*
* 枚举 {@link IotDeviceMessageMethodEnum}
* 例如说thing.property.report 属性上报
* 例如说thing.property.post 属性上报
*/
private String method;
/**
@@ -149,20 +149,12 @@ public class IotDeviceMessage {
public static IotDeviceMessage buildStateUpdateOnline() {
return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(),
MapUtil.of("state", IotDeviceStateEnum.ONLINE.getState()));
new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.ONLINE.getState()));
}
public static IotDeviceMessage buildStateOffline() {
return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(),
MapUtil.of("state", IotDeviceStateEnum.OFFLINE.getState()));
}
public static IotDeviceMessage buildOtaUpgrade(String version, String fileUrl, Long fileSize,
String fileDigestAlgorithm, String fileDigestValue) {
return requestOf(IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), MapUtil.builder()
.put("version", version).put("fileUrl", fileUrl).put("fileSize", fileSize)
.put("fileDigestAlgorithm", fileDigestAlgorithm).put("fileDigestValue", fileDigestValue)
.build());
new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.OFFLINE.getState()));
}
}

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>

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

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

@@ -6,6 +6,11 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* IoT 网关配置类
*
* @author 芋道源码
*/
@Configuration
@EnableConfigurationProperties(IotGatewayProperties.class)
public class IotGatewayConfiguration {

View File

@@ -4,6 +4,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
@@ -166,6 +168,18 @@ public class IotGatewayProperties {
@Valid
private IotEmqxConfig emqx;
/**
* Modbus TCP Client 协议配置
*/
@Valid
private IotModbusTcpClientConfig modbusTcpClient;
/**
* Modbus TCP Server 协议配置
*/
@Valid
private IotModbusTcpServerConfig modbusTcpServer;
}
/**

View File

@@ -17,7 +17,7 @@ import lombok.extern.slf4j.Slf4j;
*/
@AllArgsConstructor
@Slf4j
public abstract class IotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
public abstract class AbstractIotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotProtocol protocol;

View File

@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
@@ -112,6 +114,10 @@ public class IotProtocolManager implements SmartLifecycle {
return createMqttProtocol(config);
case EMQX:
return createEmqxProtocol(config);
case MODBUS_TCP_CLIENT:
return createModbusTcpClientProtocol(config);
case MODBUS_TCP_SERVER:
return createModbusTcpServerProtocol(config);
default:
throw new IllegalArgumentException(String.format(
"[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
@@ -188,4 +194,24 @@ public class IotProtocolManager implements SmartLifecycle {
return new IotEmqxProtocol(config);
}
/**
* 创建 Modbus TCP Client 协议实例
*
* @param config 协议实例配置
* @return Modbus TCP Client 协议实例
*/
private IotModbusTcpClientProtocol createModbusTcpClientProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotModbusTcpClientProtocol(config);
}
/**
* 创建 Modbus TCP Server 协议实例
*
* @param config 协议实例配置
* @return Modbus TCP Server 协议实例
*/
private IotModbusTcpServerProtocol createModbusTcpServerProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotModbusTcpServerProtocol(config);
}
}

View File

@@ -64,17 +64,13 @@ public class IotCoapProtocol implements IotProtocol {
/**
* 下行消息订阅者
*/
private final IotCoapDownstreamSubscriber downstreamSubscriber;
private IotCoapDownstreamSubscriber downstreamSubscriber;
public IotCoapProtocol(ProtocolProperties properties) {
IotCoapConfig coapConfig = properties.getCoap();
Assert.notNull(coapConfig, "CoAP 协议配置coap不能为空");
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
// 初始化下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus);
}
@Override
@@ -94,9 +90,9 @@ public class IotCoapProtocol implements IotProtocol {
return;
}
IotCoapConfig coapConfig = properties.getCoap();
try {
// 1.1 创建 CoAP 配置
IotCoapConfig coapConfig = properties.getCoap();
Configuration config = Configuration.createStandardWithoutFile();
config.set(CoapConfig.COAP_PORT, properties.getPort());
config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize());
@@ -131,13 +127,12 @@ public class IotCoapProtocol implements IotProtocol {
getId(), properties.getPort(), serverId);
// 4. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e);
if (coapServer != null) {
coapServer.destroy();
coapServer = null;
}
stop0();
throw e;
}
}
@@ -147,12 +142,19 @@ public class IotCoapProtocol implements IotProtocol {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
try {
downstreamSubscriber.stop();
log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e);
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2. 关闭 CoAP 服务器

View File

@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
import lombok.extern.slf4j.Slf4j;
@@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
* @author 芋道源码
*/
@Slf4j
public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
public class IotCoapDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) {
super(protocol, messageBus);

View File

@@ -90,7 +90,7 @@ public class IotEmqxProtocol implements IotProtocol {
/**
* 下行消息订阅者
*/
private final IotEmqxDownstreamSubscriber downstreamSubscriber;
private IotEmqxDownstreamSubscriber downstreamSubscriber;
public IotEmqxProtocol(ProtocolProperties properties) {
Assert.notNull(properties, "协议实例配置不能为空");
@@ -101,10 +101,6 @@ public class IotEmqxProtocol implements IotProtocol {
"MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空");
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
this.upstreamHandler = new IotEmqxUpstreamHandler(serverId);
// 初始化下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus);
}
@Override
@@ -124,7 +120,7 @@ public class IotEmqxProtocol implements IotProtocol {
return;
}
// 1.1 创建 Vertx 实例
// 1.1 创建 Vertx 实例 和 下行消息订阅者
this.vertx = Vertx.vertx();
try {
@@ -138,6 +134,8 @@ public class IotEmqxProtocol implements IotProtocol {
getId(), properties.getPort(), serverId);
// 2. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e);
@@ -157,11 +155,14 @@ public class IotEmqxProtocol implements IotProtocol {
private void stop0() {
// 1. 停止下行消息订阅者
try {
downstreamSubscriber.stop();
log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e);
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2.1 先置为 false避免 closeHandler 触发重连

View File

@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
import lombok.extern.slf4j.Slf4j;
@@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
* @author 芋道源码
*/
@Slf4j
public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
public class IotEmqxDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
private final IotEmqxDownstreamHandler downstreamHandler;

View File

@@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.vertx.mqtt.messages.MqttPublishMessage;
import lombok.extern.slf4j.Slf4j;
@@ -42,12 +43,14 @@ public class IotEmqxUpstreamHandler {
return;
}
// 2. 反序列化消息
// 2.1 反序列化消息
IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
if (message == null) {
log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
return;
}
// 2.2 标准化回复消息的 methodMQTT 协议中,设备回复消息的 method 会携带 _reply 后缀)
IotMqttTopicUtils.normalizeReplyMethod(message);
// 3. 发送消息到队列
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);

View File

@@ -64,10 +64,6 @@ public class IotHttpProtocol implements IotProtocol {
public IotHttpProtocol(ProtocolProperties properties) {
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
// 初始化下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus);
}
@Override
@@ -87,7 +83,7 @@ public class IotHttpProtocol implements IotProtocol {
return;
}
// 1.1 创建 Vertx 实例(每个 Protocol 独立管理)
// 1.1 创建 Vertx 实例
this.vertx = Vertx.vertx();
// 1.2 创建路由
@@ -123,18 +119,12 @@ public class IotHttpProtocol implements IotProtocol {
getId(), properties.getPort(), serverId);
// 2. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e);
// 启动失败时关闭资源
if (httpServer != null) {
httpServer.close();
httpServer = null;
}
if (vertx != null) {
vertx.close();
vertx = null;
}
stop0();
throw e;
}
}
@@ -144,6 +134,10 @@ public class IotHttpProtocol implements IotProtocol {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
if (downstreamSubscriber != null) {
try {

View File

@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import lombok.extern.slf4j.Slf4j;
/**
@@ -13,7 +13,7 @@ import lombok.extern.slf4j.Slf4j;
*/
@Slf4j
public class IotHttpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
public class IotHttpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) {
super(protocol, messageBus);

View File

@@ -0,0 +1,278 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import io.vertx.core.Vertx;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
/**
* Modbus 轮询调度器基类
* <p>
* 封装通用的定时器管理、per-device 请求队列限速逻辑。
* 子类只需实现 {@link #pollPoint(Long, Long)} 定义具体的轮询动作。
* <p>
*
* @author 芋道源码
*/
@Slf4j
public abstract class AbstractIotModbusPollScheduler {
protected final Vertx vertx;
/**
* 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积
*/
private static final long MIN_REQUEST_INTERVAL = 1000;
/**
* 每个设备请求队列的最大长度,超出时丢弃最旧请求
*/
private static final int MAX_QUEUE_SIZE = 1000;
/**
* 设备点位的定时器映射deviceId -> (pointId -> PointTimerInfo)
*/
private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>();
/**
* per-device 请求队列deviceId -> 待执行请求队列
*/
private final Map<Long, Queue<Runnable>> deviceRequestQueues = new ConcurrentHashMap<>();
/**
* per-device 上次请求时间戳deviceId -> lastRequestTimeMs
*/
private final Map<Long, Long> deviceLastRequestTime = new ConcurrentHashMap<>();
/**
* per-device 延迟 timer 标记deviceId -> 是否有延迟 timer 在等待
*/
private final Map<Long, Boolean> deviceDelayTimerActive = new ConcurrentHashMap<>();
protected AbstractIotModbusPollScheduler(Vertx vertx) {
this.vertx = vertx;
}
/**
* 点位定时器信息
*/
@Data
@AllArgsConstructor
private static class PointTimerInfo {
/**
* Vert.x 定时器 ID
*/
private Long timerId;
/**
* 轮询间隔(用于判断是否需要更新定时器)
*/
private Integer pollInterval;
}
// ========== 轮询管理 ==========
/**
* 更新轮询任务(增量更新)
*
* 1. 【删除】点位:停止对应的轮询定时器
* 2. 【新增】点位:创建对应的轮询定时器
* 3. 【修改】点位pollInterval 变化,重建对应的轮询定时器
* 【修改】其他属性变化不需要重建定时器pollPoint 运行时从 configCache 取最新 point
*/
public void updatePolling(IotModbusDeviceConfigRespDTO config) {
Long deviceId = config.getDeviceId();
List<IotModbusPointRespDTO> newPoints = config.getPoints();
Map<Long, PointTimerInfo> currentTimers = devicePointTimers
.computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
// 1.1 计算新配置中的点位 ID 集合
Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId);
// 1.2 计算删除的点位 ID 集合
Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet());
removedPointIds.removeAll(newPointIds);
// 2. 处理删除的点位:停止不再存在的定时器
for (Long pointId : removedPointIds) {
PointTimerInfo timerInfo = currentTimers.remove(pointId);
if (timerInfo != null) {
vertx.cancelTimer(timerInfo.getTimerId());
log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId);
}
}
// 3. 处理新增和修改的点位
if (CollUtil.isEmpty(newPoints)) {
return;
}
for (IotModbusPointRespDTO point : newPoints) {
Long pointId = point.getId();
Integer newPollInterval = point.getPollInterval();
PointTimerInfo existingTimer = currentTimers.get(pointId);
// 3.1 新增点位:创建定时器
if (existingTimer == null) {
Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
if (timerId != null) {
currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
deviceId, pointId, newPollInterval);
}
} else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
// 3.2 pollInterval 变化:重建定时器
vertx.cancelTimer(existingTimer.getTimerId());
Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
if (timerId != null) {
currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
deviceId, pointId, existingTimer.getPollInterval(), newPollInterval);
} else {
currentTimers.remove(pointId);
}
}
// 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point自动使用新配置
}
}
/**
* 创建轮询定时器
*/
private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) {
if (pollInterval == null || pollInterval <= 0) {
return null;
}
return vertx.setPeriodic(pollInterval, timerId -> {
try {
submitPollRequest(deviceId, pointId);
} catch (Exception e) {
log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e);
}
});
}
// ========== 请求队列per-device 限速) ==========
/**
* 提交轮询请求到设备请求队列(保证同设备请求间隔)
*/
private void submitPollRequest(Long deviceId, Long pointId) {
// 1. 【重要】将请求添加到设备的请求队列
Queue<Runnable> queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>());
while (queue.size() >= MAX_QUEUE_SIZE) {
// 超出上限时,丢弃最旧的请求
queue.poll();
log.warn("[submitPollRequest][设备 {} 请求队列已满({}), 丢弃最旧请求]", deviceId, MAX_QUEUE_SIZE);
}
queue.offer(() -> pollPoint(deviceId, pointId));
// 2. 处理设备请求队列(如果没有延迟 timer 在等待)
processDeviceQueue(deviceId);
}
/**
* 处理设备请求队列
*/
private void processDeviceQueue(Long deviceId) {
Queue<Runnable> queue = deviceRequestQueues.get(deviceId);
if (CollUtil.isEmpty(queue)) {
return;
}
// 检查是否已有延迟 timer 在等待
if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) {
return;
}
// 不满足间隔要求,延迟执行
long now = System.currentTimeMillis();
long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L);
long elapsed = now - lastTime;
if (elapsed < MIN_REQUEST_INTERVAL) {
scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed);
return;
}
// 满足间隔要求,立即执行
Runnable task = queue.poll();
if (task == null) {
return;
}
deviceLastRequestTime.put(deviceId, now);
task.run();
// 继续处理队列中的下一个(如果有的话,需要延迟)
if (CollUtil.isNotEmpty(queue)) {
scheduleNextRequest(deviceId);
}
}
private void scheduleNextRequest(Long deviceId) {
scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL);
}
private void scheduleNextRequest(Long deviceId, long delayMs) {
deviceDelayTimerActive.put(deviceId, true);
vertx.setTimer(delayMs, id -> {
deviceDelayTimerActive.put(deviceId, false);
Queue<Runnable> queue = deviceRequestQueues.get(deviceId);
if (CollUtil.isEmpty(queue)) {
return;
}
// 满足间隔要求,立即执行
Runnable task = queue.poll();
if (task == null) {
return;
}
deviceLastRequestTime.put(deviceId, System.currentTimeMillis());
task.run();
// 继续处理队列中的下一个(如果有的话,需要延迟)
if (CollUtil.isNotEmpty(queue)) {
scheduleNextRequest(deviceId);
}
});
}
// ========== 轮询执行 ==========
/**
* 轮询单个点位(子类实现具体的读取逻辑)
*
* @param deviceId 设备 ID
* @param pointId 点位 ID
*/
protected abstract void pollPoint(Long deviceId, Long pointId);
// ========== 停止 ==========
/**
* 停止设备的轮询
*/
public void stopPolling(Long deviceId) {
Map<Long, PointTimerInfo> timers = devicePointTimers.remove(deviceId);
if (CollUtil.isEmpty(timers)) {
return;
}
for (PointTimerInfo timerInfo : timers.values()) {
vertx.cancelTimer(timerInfo.getTimerId());
}
// 清理请求队列
deviceRequestQueues.remove(deviceId);
deviceLastRequestTime.remove(deviceId);
deviceDelayTimerActive.remove(deviceId);
log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size());
}
/**
* 停止所有轮询
*/
public void stopAll() {
for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) {
stopPolling(deviceId);
}
}
}

View File

@@ -0,0 +1,557 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* IoT Modbus 协议工具类
* <p>
* 提供 Modbus 协议全链路能力:
* <ul>
* <li>协议常量功能码FC01~FC16、异常掩码等</li>
* <li>功能码判断:读/写/异常分类、可写判断、写功能码映射</li>
* <li>CRC-16/MODBUS 计算和校验</li>
* <li>数据转换:原始值 ↔ 物模型属性值({@link #convertToPropertyValue} / {@link #convertToRawValues}</li>
* <li>帧值提取:从 Modbus 帧提取寄存器/线圈值({@link #extractValues}</li>
* <li>点位查找({@link #findPoint}</li>
* </ul>
*
* @author 芋道源码
*/
@UtilityClass
@Slf4j
public class IotModbusCommonUtils {
/** FC01: 读线圈 */
public static final int FC_READ_COILS = 1;
/** FC02: 读离散输入 */
public static final int FC_READ_DISCRETE_INPUTS = 2;
/** FC03: 读保持寄存器 */
public static final int FC_READ_HOLDING_REGISTERS = 3;
/** FC04: 读输入寄存器 */
public static final int FC_READ_INPUT_REGISTERS = 4;
/** FC05: 写单个线圈 */
public static final int FC_WRITE_SINGLE_COIL = 5;
/** FC06: 写单个寄存器 */
public static final int FC_WRITE_SINGLE_REGISTER = 6;
/** FC15: 写多个线圈 */
public static final int FC_WRITE_MULTIPLE_COILS = 15;
/** FC16: 写多个寄存器 */
public static final int FC_WRITE_MULTIPLE_REGISTERS = 16;
/**
* 异常响应掩码:响应帧的功能码最高位为 1 时,表示异常响应
* 例如:请求 FC=0x03异常响应 FC=0x830x03 | 0x80
*/
public static final int FC_EXCEPTION_MASK = 0x80;
/**
* 功能码掩码:用于从异常响应中提取原始功能码
* 例如:异常 FC=0x83原始 FC = 0x83 & 0x7F = 0x03
*/
public static final int FC_MASK = 0x7F;
// ==================== 功能码分类判断 ====================
/**
* 判断是否为读响应FC01-04
*/
public static boolean isReadResponse(int functionCode) {
return functionCode >= FC_READ_COILS && functionCode <= FC_READ_INPUT_REGISTERS;
}
/**
* 判断是否为写响应FC05/06/15/16
*/
public static boolean isWriteResponse(int functionCode) {
return functionCode == FC_WRITE_SINGLE_COIL || functionCode == FC_WRITE_SINGLE_REGISTER
|| functionCode == FC_WRITE_MULTIPLE_COILS || functionCode == FC_WRITE_MULTIPLE_REGISTERS;
}
/**
* 判断是否为异常响应
*/
public static boolean isExceptionResponse(int functionCode) {
return (functionCode & FC_EXCEPTION_MASK) != 0;
}
/**
* 从异常响应中提取原始功能码
*/
public static int extractOriginalFunctionCode(int exceptionFunctionCode) {
return exceptionFunctionCode & FC_MASK;
}
/**
* 判断读功能码是否支持写操作
* <p>
* FC01读线圈和 FC03读保持寄存器支持写操作
* FC02读离散输入和 FC04读输入寄存器为只读。
*
* @param readFunctionCode 读功能码FC01-04
* @return 是否支持写操作
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isWritable(int readFunctionCode) {
return readFunctionCode == FC_READ_COILS || readFunctionCode == FC_READ_HOLDING_REGISTERS;
}
/**
* 获取单写功能码
* <p>
* FC01读线圈→ FC05写单个线圈
* FC03读保持寄存器→ FC06写单个寄存器
* 其他返回 null不支持写
*
* @param readFunctionCode 读功能码
* @return 单写功能码,不支持写时返回 null
*/
@SuppressWarnings("EnhancedSwitchMigration")
public static Integer getWriteSingleFunctionCode(int readFunctionCode) {
switch (readFunctionCode) {
case FC_READ_COILS:
return FC_WRITE_SINGLE_COIL;
case FC_READ_HOLDING_REGISTERS:
return FC_WRITE_SINGLE_REGISTER;
default:
return null;
}
}
/**
* 获取多写功能码
* <p>
* FC01读线圈→ FC15写多个线圈
* FC03读保持寄存器→ FC16写多个寄存器
* 其他返回 null不支持写
*
* @param readFunctionCode 读功能码
* @return 多写功能码,不支持写时返回 null
*/
@SuppressWarnings("EnhancedSwitchMigration")
public static Integer getWriteMultipleFunctionCode(int readFunctionCode) {
switch (readFunctionCode) {
case FC_READ_COILS:
return FC_WRITE_MULTIPLE_COILS;
case FC_READ_HOLDING_REGISTERS:
return FC_WRITE_MULTIPLE_REGISTERS;
default:
return null;
}
}
// ==================== CRC16 工具 ====================
/**
* 计算 CRC-16/MODBUS
*
* @param data 数据
* @param length 计算长度
* @return CRC16 值
*/
public static int calculateCrc16(byte[] data, int length) {
int crc = 0xFFFF;
for (int i = 0; i < length; i++) {
crc ^= (data[i] & 0xFF);
for (int j = 0; j < 8; j++) {
if ((crc & 0x0001) != 0) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
/**
* 校验 CRC16
*
* @param data 包含 CRC 的完整数据
* @return 校验是否通过
*/
public static boolean verifyCrc16(byte[] data) {
if (data.length < 3) {
return false;
}
int computed = calculateCrc16(data, data.length - 2);
int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8);
return computed == received;
}
// ==================== 数据转换 ====================
/**
* 将原始值转换为物模型属性值
*
* @param rawValues 原始值数组(寄存器值或线圈值)
* @param point 点位配置
* @return 转换后的属性值
*/
public static Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) {
if (ArrayUtil.isEmpty(rawValues)) {
return null;
}
String rawDataType = point.getRawDataType();
String byteOrder = point.getByteOrder();
BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE);
// 1. 根据原始数据类型解析原始数值
Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder);
if (rawNumber == null) {
return null;
}
// 2. 应用缩放因子:实际值 = 原始值 × scale
BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale);
// 3. 根据数据类型返回合适的 Java 类型
return formatValue(actualValue, rawDataType);
}
/**
* 将物模型属性值转换为原始寄存器值
*
* @param propertyValue 属性值
* @param point 点位配置
* @return 原始值数组
*/
public static int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) {
if (propertyValue == null) {
return new int[0];
}
String rawDataType = point.getRawDataType();
String byteOrder = point.getByteOrder();
BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE);
int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1);
// 1. 转换为 BigDecimal
BigDecimal actualValue = new BigDecimal(propertyValue.toString());
// 2. 应用缩放因子:原始值 = 实际值 ÷ scale
BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP);
// 3. 根据原始数据类型编码为寄存器值
return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount);
}
@SuppressWarnings("EnhancedSwitchMigration")
private static Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) {
IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
if (dataTypeEnum == null) {
log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType);
return rawValues[0];
}
switch (dataTypeEnum) {
case BOOLEAN:
return rawValues[0] != 0 ? 1 : 0;
case INT16:
return (short) rawValues[0];
case UINT16:
return rawValues[0] & 0xFFFF;
case INT32:
return parseInt32(rawValues, byteOrder);
case UINT32:
return parseUint32(rawValues, byteOrder);
case FLOAT:
return parseFloat(rawValues, byteOrder);
case DOUBLE:
return parseDouble(rawValues, byteOrder);
default:
log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType);
return rawValues[0];
}
}
private static int parseInt32(int[] rawValues, String byteOrder) {
if (rawValues.length < 2) {
return rawValues[0];
}
byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt();
}
private static long parseUint32(int[] rawValues, String byteOrder) {
if (rawValues.length < 2) {
return rawValues[0] & 0xFFFFFFFFL;
}
byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL;
}
private static float parseFloat(int[] rawValues, String byteOrder) {
if (rawValues.length < 2) {
return (float) rawValues[0];
}
byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat();
}
private static double parseDouble(int[] rawValues, String byteOrder) {
if (rawValues.length < 4) {
return rawValues[0];
}
byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble();
}
private static byte[] registersToBytes(int[] registers, int count) {
byte[] bytes = new byte[count * 2];
for (int i = 0; i < Math.min(registers.length, count); i++) {
bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF);
bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF);
}
return bytes;
}
@SuppressWarnings("EnhancedSwitchMigration")
private static byte[] reorderBytes(byte[] bytes, String byteOrder) {
IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder);
// null 或者大端序,不需要调整
if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) {
return bytes;
}
// 其他字节序调整
byte[] result = new byte[bytes.length];
switch (byteOrderEnum) {
case BA: // 小端序:按每 2 字节一组交换16 位场景 [1,0]32 位场景 [1,0,3,2]
for (int i = 0; i + 1 < bytes.length; i += 2) {
result[i] = bytes[i + 1];
result[i + 1] = bytes[i];
}
break;
case CDAB: // 大端字交换32 位)
if (bytes.length >= 4) {
result[0] = bytes[2];
result[1] = bytes[3];
result[2] = bytes[0];
result[3] = bytes[1];
}
break;
case DCBA: // 小端序32 位)
if (bytes.length >= 4) {
result[0] = bytes[3];
result[1] = bytes[2];
result[2] = bytes[1];
result[3] = bytes[0];
}
break;
case BADC: // 小端字交换32 位)
if (bytes.length >= 4) {
result[0] = bytes[1];
result[1] = bytes[0];
result[2] = bytes[3];
result[3] = bytes[2];
}
break;
default:
return bytes;
}
return result;
}
@SuppressWarnings("EnhancedSwitchMigration")
private static int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) {
IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
if (dataTypeEnum == null) {
return new int[]{rawValue.intValue()};
}
switch (dataTypeEnum) {
case BOOLEAN:
return new int[]{rawValue.intValue() != 0 ? 1 : 0};
case INT16:
case UINT16:
return new int[]{rawValue.intValue() & 0xFFFF};
case INT32:
return encodeInt32(rawValue.intValue(), byteOrder);
case UINT32:
// 使用 longValue() 避免超过 Integer.MAX_VALUE 时溢出,
// 强转 int 保留低 32 位 bit pattern写入寄存器的字节是正确的无符号值
return encodeInt32((int) rawValue.longValue(), byteOrder);
case FLOAT:
return encodeFloat(rawValue.floatValue(), byteOrder);
case DOUBLE:
return encodeDouble(rawValue.doubleValue(), byteOrder);
default:
return new int[]{rawValue.intValue()};
}
}
private static int[] encodeInt32(int value, String byteOrder) {
byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array();
bytes = reorderBytes(bytes, byteOrder);
return bytesToRegisters(bytes);
}
private static int[] encodeFloat(float value, String byteOrder) {
byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array();
bytes = reorderBytes(bytes, byteOrder);
return bytesToRegisters(bytes);
}
private static int[] encodeDouble(double value, String byteOrder) {
byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array();
bytes = reorderBytes(bytes, byteOrder);
return bytesToRegisters(bytes);
}
private static int[] bytesToRegisters(byte[] bytes) {
int[] registers = new int[bytes.length / 2];
for (int i = 0; i < registers.length; i++) {
registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF);
}
return registers;
}
@SuppressWarnings("EnhancedSwitchMigration")
private static Object formatValue(BigDecimal value, String rawDataType) {
IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
if (dataTypeEnum == null) {
return value;
}
switch (dataTypeEnum) {
case BOOLEAN:
return value.intValue() != 0;
case INT16:
case INT32:
return value.intValue();
case UINT16:
case UINT32:
return value.longValue();
case FLOAT:
return value.floatValue();
case DOUBLE:
return value.doubleValue();
default:
return value;
}
}
// ==================== 帧值提取 ====================
/**
* 从帧中提取寄存器值FC01-04 读响应)
*
* @param frame 解码后的 Modbus 帧
* @return 寄存器值数组int[]),失败返回 null
*/
@SuppressWarnings("EnhancedSwitchMigration")
public static int[] extractValues(IotModbusFrame frame) {
if (frame == null || frame.isException()) {
return null;
}
byte[] pdu = frame.getPdu();
if (pdu == null || pdu.length < 1) {
return null;
}
int functionCode = frame.getFunctionCode();
switch (functionCode) {
case FC_READ_COILS:
case FC_READ_DISCRETE_INPUTS:
return extractCoilValues(pdu);
case FC_READ_HOLDING_REGISTERS:
case FC_READ_INPUT_REGISTERS:
return extractRegisterValues(pdu);
default:
log.warn("[extractValues][不支持的功能码: {}]", functionCode);
return null;
}
}
private static int[] extractCoilValues(byte[] pdu) {
if (pdu.length < 2) {
return null;
}
int byteCount = pdu[0] & 0xFF;
int bitCount = byteCount * 8;
int[] values = new int[bitCount];
for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) {
values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01);
}
return values;
}
private static int[] extractRegisterValues(byte[] pdu) {
if (pdu.length < 2) {
return null;
}
int byteCount = pdu[0] & 0xFF;
int registerCount = byteCount / 2;
int[] values = new int[registerCount];
for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) {
values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF);
}
return values;
}
/**
* 从响应帧中提取 registerCount通过 PDU 的 byteCount 推断)
*
* @param frame 解码后的 Modbus 响应帧
* @return registerCount无法提取时返回 -1匹配时跳过校验
*/
public static int extractRegisterCountFromResponse(IotModbusFrame frame) {
byte[] pdu = frame.getPdu();
if (pdu == null || pdu.length < 1) {
return -1;
}
int byteCount = pdu[0] & 0xFF;
int fc = frame.getFunctionCode();
// FC03/04 寄存器读响应registerCount = byteCount / 2
if (fc == FC_READ_HOLDING_REGISTERS || fc == FC_READ_INPUT_REGISTERS) {
return byteCount / 2;
}
// FC01/02 线圈/离散输入读响应:按 bit 打包有余位,无法精确反推,返回 -1 跳过校验
return -1;
}
// ==================== 点位查找 ====================
/**
* 查找点位配置
*
* @param config 设备 Modbus 配置
* @param identifier 点位标识符
* @return 匹配的点位配置,未找到返回 null
*/
public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) {
if (config == null || StrUtil.isBlank(identifier)) {
return null;
}
return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier()));
}
/**
* 根据点位 ID 查找点位配置
*
* @param config 设备 Modbus 配置
* @param pointId 点位 ID
* @return 匹配的点位配置,未找到返回 null
*/
public static IotModbusPointRespDTO findPointById(IotModbusDeviceConfigRespDTO config, Long pointId) {
if (config == null || pointId == null) {
return null;
}
return CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId));
}
}

View File

@@ -0,0 +1,195 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction;
import com.ghgande.j2mod.modbus.msg.*;
import com.ghgande.j2mod.modbus.procimg.InputRegister;
import com.ghgande.j2mod.modbus.procimg.Register;
import com.ghgande.j2mod.modbus.procimg.SimpleRegister;
import com.ghgande.j2mod.modbus.util.BitVector;
import io.vertx.core.Future;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.*;
/**
* IoT Modbus TCP 客户端工具类
* <p>
* 封装基于 j2mod 的 Modbus TCP 读写操作:
* 1. 根据功能码创建对应的 Modbus 读/写请求
* 2. 通过 {@link IotModbusTcpClientConnectionManager.ModbusConnection} 执行事务
* 3. 从响应中提取原始值
*
* @author 芋道源码
*/
@UtilityClass
@Slf4j
public class IotModbusTcpClientUtils {
/**
* 读取 Modbus 数据
*
* @param connection Modbus 连接
* @param slaveId 从站地址
* @param point 点位配置
* @return 原始值int 数组)
*/
public static Future<int[]> read(IotModbusTcpClientConnectionManager.ModbusConnection connection,
Integer slaveId,
IotModbusPointRespDTO point) {
return connection.executeBlocking(tcpConnection -> {
try {
// 1. 创建请求
ModbusRequest request = createReadRequest(point.getFunctionCode(),
point.getRegisterAddress(), point.getRegisterCount());
request.setUnitID(slaveId);
// 2. 执行事务(请求)
ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection);
transaction.setRequest(request);
transaction.execute();
// 3. 解析响应
ModbusResponse response = transaction.getResponse();
return extractValues(response, point.getFunctionCode());
} catch (Exception e) {
throw new RuntimeException(String.format("Modbus 读取失败 [slaveId=%d, identifier=%s, address=%d]",
slaveId, point.getIdentifier(), point.getRegisterAddress()), e);
}
});
}
/**
* 写入 Modbus 数据
*
* @param connection Modbus 连接
* @param slaveId 从站地址
* @param point 点位配置
* @param values 要写入的值
* @return 是否成功
*/
public static Future<Boolean> write(IotModbusTcpClientConnectionManager.ModbusConnection connection,
Integer slaveId,
IotModbusPointRespDTO point,
int[] values) {
return connection.executeBlocking(tcpConnection -> {
try {
// 1. 创建请求
ModbusRequest request = createWriteRequest(point.getFunctionCode(),
point.getRegisterAddress(), point.getRegisterCount(), values);
if (request == null) {
throw new RuntimeException("功能码 " + point.getFunctionCode() + " 不支持写操作");
}
request.setUnitID(slaveId);
// 2. 执行事务(请求)
ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection);
transaction.setRequest(request);
transaction.execute();
return true;
} catch (Exception e) {
throw new RuntimeException(String.format("Modbus 写入失败 [slaveId=%d, identifier=%s, address=%d]",
slaveId, point.getIdentifier(), point.getRegisterAddress()), e);
}
});
}
/**
* 创建读取请求
*/
@SuppressWarnings("EnhancedSwitchMigration")
private static ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) {
switch (functionCode) {
case FC_READ_COILS:
return new ReadCoilsRequest(address, count);
case FC_READ_DISCRETE_INPUTS:
return new ReadInputDiscretesRequest(address, count);
case FC_READ_HOLDING_REGISTERS:
return new ReadMultipleRegistersRequest(address, count);
case FC_READ_INPUT_REGISTERS:
return new ReadInputRegistersRequest(address, count);
default:
throw new IllegalArgumentException("不支持的功能码: " + functionCode);
}
}
/**
* 创建写入请求
*/
@SuppressWarnings("EnhancedSwitchMigration")
private static ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) {
switch (functionCode) {
case FC_READ_COILS: // 写线圈(使用功能码 5 或 15
if (count == 1) {
return new WriteCoilRequest(address, values[0] != 0);
} else {
BitVector bv = new BitVector(count);
for (int i = 0; i < Math.min(values.length, count); i++) {
bv.setBit(i, values[i] != 0);
}
return new WriteMultipleCoilsRequest(address, bv);
}
case FC_READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16
if (count == 1) {
return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0]));
} else {
Register[] registers = new SimpleRegister[count];
for (int i = 0; i < count; i++) {
registers[i] = new SimpleRegister(i < values.length ? values[i] : 0);
}
return new WriteMultipleRegistersRequest(address, registers);
}
case FC_READ_DISCRETE_INPUTS: // 只读
case FC_READ_INPUT_REGISTERS: // 只读
return null;
default:
throw new IllegalArgumentException("不支持的功能码: " + functionCode);
}
}
/**
* 从响应中提取值
*/
@SuppressWarnings("EnhancedSwitchMigration")
private static int[] extractValues(ModbusResponse response, Integer functionCode) {
switch (functionCode) {
case FC_READ_COILS:
ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response;
int bitCount = coilsResponse.getBitCount();
int[] coilValues = new int[bitCount];
for (int i = 0; i < bitCount; i++) {
coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0;
}
return coilValues;
case FC_READ_DISCRETE_INPUTS:
ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response;
int discreteCount = discretesResponse.getBitCount();
int[] discreteValues = new int[discreteCount];
for (int i = 0; i < discreteCount; i++) {
discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0;
}
return discreteValues;
case FC_READ_HOLDING_REGISTERS:
ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response;
InputRegister[] holdingRegisters = holdingResponse.getRegisters();
int[] holdingValues = new int[holdingRegisters.length];
for (int i = 0; i < holdingRegisters.length; i++) {
holdingValues[i] = holdingRegisters[i].getValue();
}
return holdingValues;
case FC_READ_INPUT_REGISTERS:
ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response;
InputRegister[] inputRegisters = inputResponse.getRegisters();
int[] inputValues = new int[inputRegisters.length];
for (int i = 0; i < inputRegisters.length; i++) {
inputValues[i] = inputRegisters[i].getValue();
}
return inputValues;
default:
throw new IllegalArgumentException("不支持的功能码: " + functionCode);
}
}
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* IoT Modbus TCP Client 协议配置
*
* @author 芋道源码
*/
@Data
public class IotModbusTcpClientConfig {
/**
* 配置刷新间隔(秒)
*/
@NotNull(message = "配置刷新间隔不能为空")
@Min(value = 1, message = "配置刷新间隔不能小于 1 秒")
private Integer configRefreshInterval = 30;
}

View File

@@ -0,0 +1,218 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientPollScheduler;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* IoT 网关 Modbus TCP Client 协议:主动轮询 Modbus 从站设备数据
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpClientProtocol implements IotProtocol {
/**
* 协议配置
*/
private final ProtocolProperties properties;
/**
* 服务器 ID用于消息追踪全局唯一
*/
@Getter
private final String serverId;
/**
* 运行状态
*/
@Getter
private volatile boolean running = false;
/**
* Vert.x 实例
*/
private final Vertx vertx;
/**
* 配置刷新定时器 ID
*/
private Long configRefreshTimerId;
/**
* 连接管理器
*/
private final IotModbusTcpClientConnectionManager connectionManager;
/**
* 下行消息订阅者
*/
private IotModbusTcpClientDownstreamSubscriber downstreamSubscriber;
private final IotModbusTcpClientConfigCacheService configCacheService;
private final IotModbusTcpClientPollScheduler pollScheduler;
public IotModbusTcpClientProtocol(ProtocolProperties properties) {
IotModbusTcpClientConfig modbusTcpClientConfig = properties.getModbusTcpClient();
Assert.notNull(modbusTcpClientConfig, "Modbus TCP Client 协议配置modbusTcpClient不能为空");
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
// 初始化 Vertx
this.vertx = Vertx.vertx();
// 初始化 Manager
RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class);
IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class);
this.configCacheService = new IotModbusTcpClientConfigCacheService(deviceApi);
this.connectionManager = new IotModbusTcpClientConnectionManager(redissonClient, vertx,
messageService, configCacheService, serverId);
// 初始化 Handler
IotModbusTcpClientUpstreamHandler upstreamHandler = new IotModbusTcpClientUpstreamHandler(messageService, serverId);
// 初始化轮询调度器
this.pollScheduler = new IotModbusTcpClientPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService);
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.MODBUS_TCP_CLIENT;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT Modbus TCP Client 协议 {} 已经在运行中]", getId());
return;
}
try {
// 1.1 首次加载配置
refreshConfig();
// 1.2 启动配置刷新定时器
int refreshInterval = properties.getModbusTcpClient().getConfigRefreshInterval();
configRefreshTimerId = vertx.setPeriodic(
TimeUnit.SECONDS.toMillis(refreshInterval),
id -> refreshConfig()
);
running = true;
log.info("[start][IoT Modbus TCP Client 协议 {} 启动成功serverId={}]", getId(), serverId);
// 2. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
IotModbusTcpClientDownstreamHandler downstreamHandler = new IotModbusTcpClientDownstreamHandler(connectionManager,
configCacheService);
this.downstreamSubscriber = new IotModbusTcpClientDownstreamSubscriber(this, downstreamHandler, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT Modbus TCP Client 协议 {} 启动失败]", getId(), e);
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2.1 取消配置刷新定时器
if (configRefreshTimerId != null) {
vertx.cancelTimer(configRefreshTimerId);
configRefreshTimerId = null;
}
// 2.2 停止轮询调度器
pollScheduler.stopAll();
// 2.3 关闭所有连接
connectionManager.closeAll();
// 3. 关闭 Vert.x 实例
if (vertx != null) {
try {
vertx.close().result();
log.info("[stop][IoT Modbus TCP Client 协议 {} Vertx 已关闭]", getId());
} catch (Exception e) {
log.error("[stop][IoT Modbus TCP Client 协议 {} Vertx 关闭失败]", getId(), e);
}
}
running = false;
log.info("[stop][IoT Modbus TCP Client 协议 {} 已停止]", getId());
}
/**
* 刷新配置
*/
private synchronized void refreshConfig() {
try {
// 1. 从 biz 拉取最新配置API 失败时返回 null
List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig();
if (configs == null) {
log.warn("[refreshConfig][API 失败,跳过本轮刷新]");
return;
}
log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size());
// 2. 更新连接和轮询任务
for (IotModbusDeviceConfigRespDTO config : configs) {
try {
// 2.1 确保连接存在
connectionManager.ensureConnection(config);
// 2.2 更新轮询任务
pollScheduler.updatePolling(config);
} catch (Exception e) {
log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
}
}
// 3. 清理已删除设备的资源
Set<Long> removedDeviceIds = configCacheService.cleanupRemovedDevices(configs);
for (Long deviceId : removedDeviceIds) {
pollScheduler.stopPolling(deviceId);
connectionManager.removeDevice(deviceId);
}
} catch (Exception e) {
log.error("[refreshConfig][刷新配置失败]", e);
}
}
}

View File

@@ -0,0 +1,107 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* IoT Modbus TCP Client 下行消息处理器
* <p>
* 负责:
* 1. 处理下行消息(如属性设置 thing.service.property.set
* 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusTcpClientDownstreamHandler {
private final IotModbusTcpClientConnectionManager connectionManager;
private final IotModbusTcpClientConfigCacheService configCacheService;
/**
* 处理下行消息
*/
@SuppressWarnings({"unchecked", "DuplicatedCode"})
public void handle(IotDeviceMessage message) {
// 1.1 检查是否是属性设置消息
if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) {
return;
}
if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) {
log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod());
return;
}
// 1.2 获取设备配置
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId());
if (config == null) {
log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId());
return;
}
// 2. 解析属性值并写入
Object params = message.getParams();
if (!(params instanceof Map)) {
log.warn("[handle][params 不是 Map 类型: {}]", params);
return;
}
Map<String, Object> propertyMap = (Map<String, Object>) params;
for (Map.Entry<String, Object> entry : propertyMap.entrySet()) {
String identifier = entry.getKey();
Object value = entry.getValue();
// 2.1 查找对应的点位配置
IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier);
if (point == null) {
log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
continue;
}
// 2.2 检查是否支持写操作
if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) {
log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
continue;
}
// 2.3 执行写入
writeProperty(config, point, value);
}
}
/**
* 写入属性值
*/
private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) {
// 1.1 获取连接
IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId());
if (connection == null) {
log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId());
return;
}
// 1.2 获取 slave ID
Integer slaveId = connectionManager.getSlaveId(config.getDeviceId());
if (slaveId == null) {
log.warn("[writeProperty][设备 {} 没有 slaveId]", config.getDeviceId());
return;
}
// 2.1 转换属性值为原始值
int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point);
// 2.2 执行 Modbus 写入
IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues)
.onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
config.getDeviceId(), point.getIdentifier(), value))
.onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]",
config.getDeviceId(), point.getIdentifier(), e));
}
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpClientDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
private final IotModbusTcpClientDownstreamHandler downstreamHandler;
public IotModbusTcpClientDownstreamSubscriber(IotModbusTcpClientProtocol protocol,
IotModbusTcpClientDownstreamHandler downstreamHandler,
IotMessageBus messageBus) {
super(protocol, messageBus);
this.downstreamHandler = downstreamHandler;
}
@Override
protected void handleMessage(IotDeviceMessage message) {
downstreamHandler.handle(message);
}
}

View File

@@ -0,0 +1,60 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* IoT Modbus TCP 上行数据处理器:将原始值转换为物模型属性值并上报
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpClientUpstreamHandler {
private final IotDeviceMessageService messageService;
private final String serverId;
public IotModbusTcpClientUpstreamHandler(IotDeviceMessageService messageService,
String serverId) {
this.messageService = messageService;
this.serverId = serverId;
}
/**
* 处理 Modbus 读取结果
*
* @param config 设备配置
* @param point 点位配置
* @param rawValue 原始值int 数组)
*/
public void handleReadResult(IotModbusDeviceConfigRespDTO config,
IotModbusPointRespDTO point,
int[] rawValue) {
try {
// 1.1 转换原始值为物模型属性值(点位翻译)
Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValue, point);
log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]",
config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue);
// 1.2 构造属性上报消息
Map<String, Object> params = MapUtil.of(point.getIdentifier(), convertedValue);
IotDeviceMessage message = IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
// 2. 发送到消息总线
messageService.sendDeviceMessage(message, config.getProductKey(),
config.getDeviceName(), serverId);
} catch (Exception e) {
log.error("[handleReadResult][处理读取结果失败, deviceId={}, identifier={}]",
config.getDeviceId(), point.getIdentifier(), e);
}
}
}

View File

@@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
/**
* IoT Modbus TCP Client 配置缓存服务
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusTcpClientConfigCacheService {
private final IotDeviceCommonApi deviceApi;
/**
* 配置缓存deviceId -> 配置
*/
private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>();
/**
* 已知的设备 ID 集合(作用:用于检测已删除的设备)
*
* @see #cleanupRemovedDevices(List)
*/
private final Set<Long> knownDeviceIds = ConcurrentHashMap.newKeySet();
/**
* 刷新配置
*
* @return 最新的配置列表API 失败时返回 null调用方应跳过 cleanup
*/
public List<IotModbusDeviceConfigRespDTO> refreshConfig() {
try {
// 1. 从远程获取配置
CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(
new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus())
.setMode(IotModbusModeEnum.POLLING.getMode()).setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_CLIENT.getType()));
result.checkError();
List<IotModbusDeviceConfigRespDTO> configs = result.getData();
// 2. 更新缓存(注意:不在这里更新 knownDeviceIds由 cleanupRemovedDevices 统一管理)
for (IotModbusDeviceConfigRespDTO config : configs) {
configCache.put(config.getDeviceId(), config);
}
return configs;
} catch (Exception e) {
log.error("[refreshConfig][刷新配置失败]", e);
return null;
}
}
/**
* 获取设备配置
*
* @param deviceId 设备 ID
* @return 配置
*/
public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) {
return configCache.get(deviceId);
}
/**
* 计算已删除设备的 ID 集合,清理缓存,并更新已知设备 ID 集合
*
* @param currentConfigs 当前有效的配置列表
* @return 已删除的设备 ID 集合
*/
public Set<Long> cleanupRemovedDevices(List<IotModbusDeviceConfigRespDTO> currentConfigs) {
// 1.1 获取当前有效的设备 ID
Set<Long> currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId);
// 1.2 找出已删除的设备(基于旧的 knownDeviceIds
Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds);
removedDeviceIds.removeAll(currentDeviceIds);
// 2. 清理已删除设备的缓存
for (Long deviceId : removedDeviceIds) {
log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId);
configCache.remove(deviceId);
}
// 3. 更新已知设备 ID 集合为当前有效的设备 ID
knownDeviceIds.clear();
knownDeviceIds.addAll(currentDeviceIds);
return removedDeviceIds;
}
}

View File

@@ -0,0 +1,317 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import com.ghgande.j2mod.modbus.net.TCPMasterConnection;
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* IoT Modbus TCP 连接管理器
* <p>
* 统一管理 Modbus TCP 连接:
* 1. 管理 TCP 连接(相同 ip:port 共用连接)
* 2. 分布式锁管理(连接级别),避免多节点重复创建连接
* 3. 连接重试和故障恢复
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpClientConnectionManager {
private static final String LOCK_KEY_PREFIX = "iot:modbus-tcp:connection:";
private final RedissonClient redissonClient;
private final Vertx vertx;
private final IotDeviceMessageService messageService;
private final IotModbusTcpClientConfigCacheService configCacheService;
private final String serverId;
/**
* 连接池key = ip:port
*/
private final Map<String, ModbusConnection> connectionPool = new ConcurrentHashMap<>();
/**
* 设备 ID 到连接 key 的映射
*/
private final Map<Long, String> deviceConnectionMap = new ConcurrentHashMap<>();
public IotModbusTcpClientConnectionManager(RedissonClient redissonClient, Vertx vertx,
IotDeviceMessageService messageService,
IotModbusTcpClientConfigCacheService configCacheService,
String serverId) {
this.redissonClient = redissonClient;
this.vertx = vertx;
this.messageService = messageService;
this.configCacheService = configCacheService;
this.serverId = serverId;
}
/**
* 确保连接存在
* <p>
* 首次建连成功时,直接发送设备上线消息
*
* @param config 设备配置
*/
public void ensureConnection(IotModbusDeviceConfigRespDTO config) {
// 1.1 检查设备是否切换了 IP/端口,若是则先清理旧连接
String connectionKey = buildConnectionKey(config.getIp(), config.getPort());
String oldConnectionKey = deviceConnectionMap.get(config.getDeviceId());
if (oldConnectionKey != null && ObjUtil.notEqual(oldConnectionKey, connectionKey)) {
log.info("[ensureConnection][设备 {} IP/端口变更: {} -> {}, 清理旧连接]",
config.getDeviceId(), oldConnectionKey, connectionKey);
removeDevice(config.getDeviceId());
}
// 1.2 记录设备与连接的映射
deviceConnectionMap.put(config.getDeviceId(), connectionKey);
// 2. 情况一:连接已存在,注册设备并发送上线消息
ModbusConnection connection = connectionPool.get(connectionKey);
if (connection != null) {
addDeviceAndOnline(connection, config);
return;
}
// 3. 情况二:连接不存在,加分布式锁创建新连接
RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + connectionKey);
if (!lock.tryLock()) {
log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey);
return;
}
try {
// 3.1 double-check拿到锁后再次检查避免并发创建重复连接
connection = connectionPool.get(connectionKey);
if (connection != null) {
addDeviceAndOnline(connection, config);
lock.unlock();
return;
}
// 3.2 创建新连接
connection = createConnection(config);
connection.setLock(lock);
connectionPool.put(connectionKey, connection);
log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey);
// 3.3 注册设备并发送上线消息
addDeviceAndOnline(connection, config);
} catch (Exception e) {
log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e);
// 建连失败,释放锁让其他节点可重试
lock.unlock();
}
}
/**
* 创建 Modbus TCP 连接
*/
private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config) throws Exception {
// 1. 创建 TCP 连接
TCPMasterConnection tcpConnection = new TCPMasterConnection(InetAddress.getByName(config.getIp()));
tcpConnection.setPort(config.getPort());
tcpConnection.setTimeout(config.getTimeout());
tcpConnection.connect();
// 2. 创建 Modbus 连接对象
return new ModbusConnection()
.setConnectionKey(buildConnectionKey(config.getIp(), config.getPort()))
.setTcpConnection(tcpConnection).setContext(vertx.getOrCreateContext())
.setTimeout(config.getTimeout()).setRetryInterval(config.getRetryInterval());
}
/**
* 获取连接
*/
public ModbusConnection getConnection(Long deviceId) {
String connectionKey = deviceConnectionMap.get(deviceId);
if (connectionKey == null) {
return null;
}
return connectionPool.get(connectionKey);
}
/**
* 获取设备的 slave ID
*/
public Integer getSlaveId(Long deviceId) {
ModbusConnection connection = getConnection(deviceId);
if (connection == null) {
return null;
}
return connection.getSlaveId(deviceId);
}
/**
* 移除设备
* <p>
* 移除时直接发送设备下线消息
*/
public void removeDevice(Long deviceId) {
// 1.1 移除设备时,发送下线消息
sendOfflineMessage(deviceId);
// 1.2 移除设备引用
String connectionKey = deviceConnectionMap.remove(deviceId);
if (connectionKey == null) {
return;
}
// 2.1 移除连接中的设备引用
ModbusConnection connection = connectionPool.get(connectionKey);
if (connection == null) {
return;
}
connection.removeDevice(deviceId);
// 2.2 如果没有设备引用了,关闭连接
if (connection.getDeviceCount() == 0) {
closeConnection(connectionKey);
}
}
// ==================== 设备连接 & 上下线消息 ====================
/**
* 注册设备到连接,并发送上线消息
*/
private void addDeviceAndOnline(ModbusConnection connection,
IotModbusDeviceConfigRespDTO config) {
Integer previous = connection.addDevice(config.getDeviceId(), config.getSlaveId());
// 首次注册,发送上线消息
if (previous == null) {
sendOnlineMessage(config);
}
}
/**
* 发送设备上线消息
*/
private void sendOnlineMessage(IotModbusDeviceConfigRespDTO config) {
try {
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
messageService.sendDeviceMessage(onlineMessage,
config.getProductKey(), config.getDeviceName(), serverId);
} catch (Exception ex) {
log.error("[sendOnlineMessage][发送设备上线消息失败, deviceId={}]", config.getDeviceId(), ex);
}
}
/**
* 发送设备下线消息
*/
private void sendOfflineMessage(Long deviceId) {
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
if (config == null) {
return;
}
try {
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
messageService.sendDeviceMessage(offlineMessage,
config.getProductKey(), config.getDeviceName(), serverId);
} catch (Exception ex) {
log.error("[sendOfflineMessage][发送设备下线消息失败, deviceId={}]", deviceId, ex);
}
}
/**
* 关闭指定连接
*/
private void closeConnection(String connectionKey) {
ModbusConnection connection = connectionPool.remove(connectionKey);
if (connection == null) {
return;
}
try {
if (connection.getTcpConnection() != null) {
connection.getTcpConnection().close();
}
// 释放分布式锁,让其他节点可接管
RLock lock = connection.getLock();
if (lock != null && lock.isHeldByCurrentThread()) {
lock.unlock();
}
log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey);
} catch (Exception e) {
log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e);
}
}
/**
* 关闭所有连接
*/
public void closeAll() {
// 先复制再遍历,避免 closeConnection 中 remove 导致并发修改
List<String> connectionKeys = new ArrayList<>(connectionPool.keySet());
for (String connectionKey : connectionKeys) {
closeConnection(connectionKey);
}
deviceConnectionMap.clear();
}
private String buildConnectionKey(String ip, Integer port) {
return ip + ":" + port;
}
/**
* Modbus 连接信息
*/
@Data
public static class ModbusConnection {
private String connectionKey;
private TCPMasterConnection tcpConnection;
private Integer timeout;
private Integer retryInterval;
/**
* 设备 ID 到 slave ID 的映射
*/
private final Map<Long, Integer> deviceSlaveMap = new ConcurrentHashMap<>();
/**
* 分布式锁,锁住连接的创建和销毁,避免多节点重复连接同一从站
*/
private RLock lock;
/**
* Vert.x Context用于 executeBlocking 执行 Modbus 操作,保证同一连接的操作串行执行
*/
private Context context;
public Integer addDevice(Long deviceId, Integer slaveId) {
return deviceSlaveMap.putIfAbsent(deviceId, slaveId);
}
public void removeDevice(Long deviceId) {
deviceSlaveMap.remove(deviceId);
}
public int getDeviceCount() {
return deviceSlaveMap.size();
}
public Integer getSlaveId(Long deviceId) {
return deviceSlaveMap.get(deviceId);
}
/**
* 执行 Modbus 读取操作(阻塞方式,在 Vert.x worker 线程执行)
*/
public <T> Future<T> executeBlocking(java.util.function.Function<TCPMasterConnection, T> operation) {
// ordered=true 保证同一 Context 的操作串行执行,不同连接之间可并行
return context.executeBlocking(() -> operation.apply(tcpConnection), true);
}
}
}

View File

@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler;
import io.vertx.core.Vertx;
import lombok.extern.slf4j.Slf4j;
/**
* IoT Modbus TCP Client 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpClientPollScheduler extends AbstractIotModbusPollScheduler {
private final IotModbusTcpClientConnectionManager connectionManager;
private final IotModbusTcpClientUpstreamHandler upstreamHandler;
private final IotModbusTcpClientConfigCacheService configCacheService;
public IotModbusTcpClientPollScheduler(Vertx vertx,
IotModbusTcpClientConnectionManager connectionManager,
IotModbusTcpClientUpstreamHandler upstreamHandler,
IotModbusTcpClientConfigCacheService configCacheService) {
super(vertx);
this.connectionManager = connectionManager;
this.upstreamHandler = upstreamHandler;
this.configCacheService = configCacheService;
}
// ========== 轮询执行 ==========
/**
* 轮询单个点位
*/
@Override
protected void pollPoint(Long deviceId, Long pointId) {
// 1.1 从 configCache 获取最新配置
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
if (config == null || CollUtil.isEmpty(config.getPoints())) {
log.warn("[pollPoint][设备 {} 没有配置]", deviceId);
return;
}
// 1.2 查找点位
IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId);
if (point == null) {
log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId);
return;
}
// 2.1 获取连接
IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId);
if (connection == null) {
log.warn("[pollPoint][设备 {} 没有连接]", deviceId);
return;
}
// 2.2 获取 slave ID
Integer slaveId = connectionManager.getSlaveId(deviceId);
Assert.notNull(slaveId, "设备 {} 没有配置 slaveId", deviceId);
// 3. 执行 Modbus 读取
IotModbusTcpClientUtils.read(connection, slaveId, point)
.onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue))
.onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]",
deviceId, point.getIdentifier(), e));
}
}

View File

@@ -0,0 +1,6 @@
/**
* Modbus TCP Client主站协议网关主动连接并轮询 Modbus 从站设备
* <p>
* 基于 j2mod 实现,支持 FC01-04 读、FC05/06/15/16 写,定时轮询 + 下发属性设置
*/
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;

View File

@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* IoT Modbus TCP Server 协议配置
*
* @author 芋道源码
*/
@Data
public class IotModbusTcpServerConfig {
/**
* 配置刷新间隔(秒)
*/
@NotNull(message = "配置刷新间隔不能为空")
@Min(value = 1, message = "配置刷新间隔不能小于 1 秒")
private Integer configRefreshInterval = 30;
/**
* 自定义功能码(用于认证等扩展交互)
* Modbus 协议保留 65-72 给用户自定义,默认 65
*/
@NotNull(message = "自定义功能码不能为空")
@Min(value = 65, message = "自定义功能码不能小于 65")
@Max(value = 72, message = "自定义功能码不能大于 72")
private Integer customFunctionCode = 65;
/**
* Pending Request 超时时间(毫秒)
*/
@NotNull(message = "请求超时时间不能为空")
private Integer requestTimeout = 5000;
/**
* Pending Request 清理间隔(毫秒)
*/
@NotNull(message = "请求清理间隔不能为空")
private Integer requestCleanupInterval = 10000;
}

View File

@@ -0,0 +1,334 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.upstream.IotModbusTcpServerUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPollScheduler;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import io.vertx.core.net.NetServer;
import io.vertx.core.net.NetServerOptions;
import io.vertx.core.net.NetSocket;
import io.vertx.core.parsetools.RecordParser;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* IoT 网关 Modbus TCP Server 协议
* <p>
* 作为 TCP Server 接收设备主动连接:
* 1. 设备通过自定义功能码FC 65发送认证请求
* 2. 认证成功后,网关主动发送 Modbus 读请求,设备响应(云端轮询模式)
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpServerProtocol implements IotProtocol {
/**
* 协议配置
*/
private final ProtocolProperties properties;
/**
* 服务器 ID用于消息追踪全局唯一
*/
@Getter
private final String serverId;
/**
* 运行状态
*/
@Getter
private volatile boolean running = false;
/**
* Vert.x 实例
*/
private final Vertx vertx;
/**
* TCP Server
*/
private NetServer netServer;
/**
* 配置刷新定时器 ID
*/
private Long configRefreshTimerId;
/**
* Pending Request 清理定时器 ID
*/
private Long requestCleanupTimerId;
/**
* 连接管理器
*/
private final IotModbusTcpServerConnectionManager connectionManager;
/**
* 下行消息订阅者
*/
private IotModbusTcpServerDownstreamSubscriber downstreamSubscriber;
private final IotModbusFrameDecoder frameDecoder;
@SuppressWarnings("FieldCanBeLocal")
private final IotModbusFrameEncoder frameEncoder;
private final IotModbusTcpServerConfigCacheService configCacheService;
private final IotModbusTcpServerPendingRequestManager pendingRequestManager;
private final IotModbusTcpServerUpstreamHandler upstreamHandler;
private final IotModbusTcpServerPollScheduler pollScheduler;
private final IotDeviceMessageService messageService;
public IotModbusTcpServerProtocol(ProtocolProperties properties) {
IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer();
Assert.notNull(slaveConfig, "Modbus TCP Server 协议配置modbusTcpServer不能为空");
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
// 初始化 Vertx
this.vertx = Vertx.vertx();
// 初始化 Manager
IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.connectionManager = new IotModbusTcpServerConnectionManager();
this.configCacheService = new IotModbusTcpServerConfigCacheService(deviceApi);
this.pendingRequestManager = new IotModbusTcpServerPendingRequestManager();
// 初始化帧编解码器
this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode());
this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode());
// 初始化共享事务 ID 自增器PollScheduler 和 DownstreamHandler 共用,避免 transactionId 冲突)
AtomicInteger transactionIdCounter = new AtomicInteger(0);
// 初始化轮询调度器
this.pollScheduler = new IotModbusTcpServerPollScheduler(
vertx, connectionManager, frameEncoder, pendingRequestManager,
slaveConfig.getRequestTimeout(), transactionIdCounter, configCacheService);
// 初始化 Handler
this.messageService = SpringUtil.getBean(IotDeviceMessageService.class);
IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class);
this.upstreamHandler = new IotModbusTcpServerUpstreamHandler(
deviceApi, this.messageService, frameEncoder,
connectionManager, configCacheService, pendingRequestManager,
pollScheduler, deviceService, serverId);
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.MODBUS_TCP_SERVER;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT Modbus TCP Server 协议 {} 已经在运行中]", getId());
return;
}
try {
// 1. 启动配置刷新定时器
IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer();
configRefreshTimerId = vertx.setPeriodic(
TimeUnit.SECONDS.toMillis(slaveConfig.getConfigRefreshInterval()),
id -> refreshConfig());
// 2.1 启动 TCP Server
startTcpServer();
// 2.2 启动 PendingRequest 清理定时器
requestCleanupTimerId = vertx.setPeriodic(
slaveConfig.getRequestCleanupInterval(),
id -> pendingRequestManager.cleanupExpired());
running = true;
log.info("[start][IoT Modbus TCP Server 协议 {} 启动成功, serverId={}, port={}]",
getId(), serverId, properties.getPort());
// 3. 启动下行消息订阅
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
IotModbusTcpServerDownstreamHandler downstreamHandler = new IotModbusTcpServerDownstreamHandler(
connectionManager, configCacheService, frameEncoder, this.pollScheduler.getTransactionIdCounter());
this.downstreamSubscriber = new IotModbusTcpServerDownstreamSubscriber(
this, downstreamHandler, messageBus);
downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT Modbus TCP Server 协议 {} 启动失败]", getId(), e);
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
} catch (Exception e) {
log.error("[stop][下行消息订阅器停止失败]", e);
}
downstreamSubscriber = null;
}
// 2.1 取消定时器
if (configRefreshTimerId != null) {
vertx.cancelTimer(configRefreshTimerId);
configRefreshTimerId = null;
}
if (requestCleanupTimerId != null) {
vertx.cancelTimer(requestCleanupTimerId);
requestCleanupTimerId = null;
}
// 2.2 停止轮询
pollScheduler.stopAll();
// 2.3 清理 PendingRequest
pendingRequestManager.clear();
// 2.4 关闭所有连接
connectionManager.closeAll();
// 2.5 关闭 TCP Server
if (netServer != null) {
try {
netServer.close().result();
log.info("[stop][TCP Server 已关闭]");
} catch (Exception e) {
log.error("[stop][TCP Server 关闭失败]", e);
}
netServer = null;
}
// 3. 关闭 Vertx
if (vertx != null) {
try {
vertx.close().result();
} catch (Exception e) {
log.error("[stop][Vertx 关闭失败]", e);
}
}
running = false;
log.info("[stop][IoT Modbus TCP Server 协议 {} 已停止]", getId());
}
/**
* 启动 TCP Server
*/
private void startTcpServer() {
// 1. 创建 TCP Server
NetServerOptions options = new NetServerOptions()
.setPort(properties.getPort());
netServer = vertx.createNetServer(options);
// 2. 设置连接处理器
netServer.connectHandler(this::handleConnection);
try {
netServer.listen().toCompletionStage().toCompletableFuture().get();
log.info("[startTcpServer][TCP Server 启动成功, port={}]", properties.getPort());
} catch (Exception e) {
throw new RuntimeException("[startTcpServer][TCP Server 启动失败]", e);
}
}
/**
* 处理新连接
*/
private void handleConnection(NetSocket socket) {
log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress());
// 1. 创建 RecordParser 并设置为数据处理器
RecordParser recordParser = frameDecoder.createRecordParser((frame, frameFormat) -> {
// 【重要】帧处理分发,即消息处理
upstreamHandler.handleFrame(socket, frame, frameFormat);
});
socket.handler(recordParser);
// 2.1 连接关闭处理
socket.closeHandler(v -> {
ConnectionInfo info = connectionManager.removeConnection(socket);
if (info == null || info.getDeviceId() == null) {
log.info("[handleConnection][未认证连接关闭, remoteAddress={}]", socket.remoteAddress());
return;
}
pollScheduler.stopPolling(info.getDeviceId());
pendingRequestManager.removeDevice(info.getDeviceId());
configCacheService.removeConfig(info.getDeviceId());
// 发送设备下线消息
try {
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
messageService.sendDeviceMessage(offlineMessage, info.getProductKey(), info.getDeviceName(), serverId);
} catch (Exception ex) {
log.error("[handleConnection][发送设备下线消息失败, deviceId={}]", info.getDeviceId(), ex);
}
log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]",
info.getDeviceId(), socket.remoteAddress());
});
// 2.2 异常处理
socket.exceptionHandler(e -> {
log.error("[handleConnection][连接异常, remoteAddress={}]", socket.remoteAddress(), e);
socket.close();
});
}
/**
* 刷新已连接设备的配置(定时调用)
*/
private synchronized void refreshConfig() {
try {
// 1. 只刷新已连接设备的配置
Set<Long> connectedDeviceIds = connectionManager.getConnectedDeviceIds();
if (CollUtil.isEmpty(connectedDeviceIds)) {
return;
}
List<IotModbusDeviceConfigRespDTO> configs =
configCacheService.refreshConnectedDeviceConfigList(connectedDeviceIds);
if (configs == null) {
log.warn("[refreshConfig][刷新配置失败,跳过本次刷新]");
return;
}
log.debug("[refreshConfig][刷新了 {} 个已连接设备的配置]", configs.size());
// 2. 更新已连接设备的轮询任务
for (IotModbusDeviceConfigRespDTO config : configs) {
try {
pollScheduler.updatePolling(config);
} catch (Exception e) {
log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
}
}
} catch (Exception e) {
log.error("[refreshConfig][刷新配置失败]", e);
}
}
}

View File

@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* IoT Modbus 统一帧数据模型TCP/RTU 公用)
*
* @author 芋道源码
*/
@Data
@Accessors(chain = true)
public class IotModbusFrame {
/**
* 从站地址
*/
private int slaveId;
/**
* 功能码
*/
private int functionCode;
/**
* PDU 数据(不含 slaveId
*/
private byte[] pdu;
/**
* 事务标识符
* <p>
* 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值
*/
private Integer transactionId;
/**
* 异常码
* <p>
* 当功能码最高位为 1 时(异常响应),此字段存储异常码。
*
* @see IotModbusCommonUtils#FC_EXCEPTION_MASK
*/
private Integer exceptionCode;
/**
* 自定义功能码时的 JSON 字符串(用于 auth 认证等等)
*/
private String customData;
/**
* 是否异常响应(基于 exceptionCode 是否有值判断)
*/
public boolean isException() {
return exceptionCode != null;
}
}

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