【功能修改】IoT: 修改网络组件模块,包含 HTTP 和 EMQX 组件,重构相关配置和处理逻辑,更新文档说明。
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
<modules>
|
<modules>
|
||||||
<module>yudao-module-iot-api</module>
|
<module>yudao-module-iot-api</module>
|
||||||
<module>yudao-module-iot-biz</module>
|
<module>yudao-module-iot-biz</module>
|
||||||
<module>yudao-module-iot-components</module>
|
<module>yudao-module-iot-net-components</module>
|
||||||
<!-- <module>yudao-module-iot-plugins</module>-->
|
<!-- <module>yudao-module-iot-plugins</module>-->
|
||||||
</modules>
|
</modules>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -93,10 +93,10 @@
|
|||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<!-- <dependency>-->
|
||||||
<groupId>org.pf4j</groupId> <!-- PF4J:内置插件机制 -->
|
<!-- <groupId>org.pf4j</groupId> <!– PF4J:内置插件机制 –>-->
|
||||||
<artifactId>pf4j-spring</artifactId>
|
<!-- <artifactId>pf4j-spring</artifactId>-->
|
||||||
</dependency>
|
<!-- </dependency>-->
|
||||||
|
|
||||||
<!-- TODO @芋艿:bom 管理 -->
|
<!-- TODO @芋艿:bom 管理 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -137,12 +137,12 @@
|
|||||||
<!-- IoT 网络组件:接收来自设备的上行数据 -->
|
<!-- IoT 网络组件:接收来自设备的上行数据 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-module-iot-component-http</artifactId>
|
<artifactId>yudao-module-iot-net-component-http</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-module-iot-component-emqx</artifactId>
|
<artifactId>yudao-module-iot-net-component-emqx</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
|||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@Validated
|
@Validated
|
||||||
@Primary // 保证优先匹配,因为 yudao-module-iot-component-core 也有 IotDeviceUpstreamApi 的实现,并且也可能会被 biz 引入
|
@Primary // 保证优先匹配,因为 yudao-module-iot-net-component-core 也有 IotDeviceUpstreamApi 的实现,并且也可能会被 biz 引入
|
||||||
public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi {
|
public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package cn.iocoder.yudao.module.iot.framework.plugin.config;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.module.iot.framework.plugin.core.IotPluginStartRunner;
|
|
||||||
import cn.iocoder.yudao.module.iot.framework.plugin.core.IotPluginStateListener;
|
|
||||||
import cn.iocoder.yudao.module.iot.service.plugin.IotPluginConfigService;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.pf4j.spring.SpringPluginManager;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IoT 插件配置类
|
|
||||||
*
|
|
||||||
* @author haohao
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@Slf4j
|
|
||||||
public class IotPluginConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public IotPluginStartRunner pluginStartRunner(SpringPluginManager pluginManager,
|
|
||||||
IotPluginConfigService pluginConfigService) {
|
|
||||||
return new IotPluginStartRunner(pluginManager, pluginConfigService);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @芋艿:需要 review 下
|
|
||||||
@Bean
|
|
||||||
public SpringPluginManager pluginManager(@Value("${pf4j.pluginsDir:pluginsDir}") String pluginsDir) {
|
|
||||||
log.info("[init][实例化 SpringPluginManager]");
|
|
||||||
SpringPluginManager springPluginManager = new SpringPluginManager(Paths.get(pluginsDir)) {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startPlugins() {
|
|
||||||
// 禁用插件启动,避免插件启动时,启动所有插件
|
|
||||||
log.info("[init][禁用默认启动所有插件]");
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
springPluginManager.addPluginStateListener(new IotPluginStateListener());
|
|
||||||
return springPluginManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package cn.iocoder.yudao.module.iot.framework.plugin.core;
|
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
|
||||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
|
||||||
import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO;
|
|
||||||
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginDeployTypeEnum;
|
|
||||||
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
|
|
||||||
import cn.iocoder.yudao.module.iot.service.plugin.IotPluginConfigService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.pf4j.spring.SpringPluginManager;
|
|
||||||
import org.springframework.boot.ApplicationArguments;
|
|
||||||
import org.springframework.boot.ApplicationRunner;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IoT 插件启动 Runner
|
|
||||||
*
|
|
||||||
* 用于 Spring Boot 启动时,启动 {@link IotPluginDeployTypeEnum#JAR} 部署类型的插件
|
|
||||||
*/
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
|
||||||
public class IotPluginStartRunner implements ApplicationRunner {
|
|
||||||
|
|
||||||
private final SpringPluginManager springPluginManager;
|
|
||||||
|
|
||||||
private final IotPluginConfigService pluginConfigService;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run(ApplicationArguments args) {
|
|
||||||
List<IotPluginConfigDO> pluginConfigList = TenantUtils.executeIgnore(
|
|
||||||
() -> pluginConfigService.getPluginConfigListByStatusAndDeployType(
|
|
||||||
IotPluginStatusEnum.RUNNING.getStatus(), IotPluginDeployTypeEnum.JAR.getDeployType()));
|
|
||||||
if (CollUtil.isEmpty(pluginConfigList)) {
|
|
||||||
log.info("[run][没有需要启动的插件]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 遍历插件列表,逐个启动
|
|
||||||
pluginConfigList.forEach(pluginConfig -> {
|
|
||||||
try {
|
|
||||||
log.info("[run][插件({}) 启动开始]", pluginConfig.getPluginKey());
|
|
||||||
springPluginManager.startPlugin(pluginConfig.getPluginKey());
|
|
||||||
log.info("[run][插件({}) 启动完成]", pluginConfig.getPluginKey());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[run][插件({}) 启动异常]", pluginConfig.getPluginKey(), e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package cn.iocoder.yudao.module.iot.framework.plugin.core;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.pf4j.PluginStateEvent;
|
|
||||||
import org.pf4j.PluginStateListener;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IoT 插件状态监听器,用于 log 插件的状态变化
|
|
||||||
*
|
|
||||||
* @author haohao
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class IotPluginStateListener implements PluginStateListener {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void pluginStateChanged(PluginStateEvent event) {
|
|
||||||
log.info("[pluginStateChanged][插件({}) 状态变化,从 {} 变为 {}]", event.getPlugin().getPluginId(),
|
|
||||||
event.getOldState().toString(), event.getPluginState().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,6 @@ import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginConfigMapper;
|
|||||||
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
|
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.pf4j.PluginWrapper;
|
|
||||||
import org.pf4j.spring.SpringPluginManager;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -35,8 +33,8 @@ public class IotPluginConfigServiceImpl implements IotPluginConfigService {
|
|||||||
@Resource
|
@Resource
|
||||||
private IotPluginInstanceService pluginInstanceService;
|
private IotPluginInstanceService pluginInstanceService;
|
||||||
|
|
||||||
@Resource
|
// @Resource
|
||||||
private SpringPluginManager springPluginManager;
|
// private SpringPluginManager springPluginManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long createPluginConfig(PluginConfigSaveReqVO createReqVO) {
|
public Long createPluginConfig(PluginConfigSaveReqVO createReqVO) {
|
||||||
@@ -130,16 +128,16 @@ public class IotPluginConfigServiceImpl implements IotPluginConfigService {
|
|||||||
validatePluginConfigFile(pluginKeyNew);
|
validatePluginConfigFile(pluginKeyNew);
|
||||||
|
|
||||||
// 4. 更新插件配置
|
// 4. 更新插件配置
|
||||||
IotPluginConfigDO updatedPluginConfig = new IotPluginConfigDO()
|
// IotPluginConfigDO updatedPluginConfig = new IotPluginConfigDO()
|
||||||
.setId(pluginConfigDO.getId())
|
// .setId(pluginConfigDO.getId())
|
||||||
.setPluginKey(pluginKeyNew)
|
// .setPluginKey(pluginKeyNew)
|
||||||
.setStatus(IotPluginStatusEnum.STOPPED.getStatus()) // TODO @haohao:这个状态,是不是非 stop 哈?
|
// .setStatus(IotPluginStatusEnum.STOPPED.getStatus()) // TODO @haohao:这个状态,是不是非 stop 哈?
|
||||||
.setFileName(file.getOriginalFilename())
|
// .setFileName(file.getOriginalFilename())
|
||||||
.setScript("") // TODO @haohao:这个设置为 "" 会不会覆盖数据里的哈?应该从插件里读取?未来?
|
// .setScript("") // TODO @haohao:这个设置为 "" 会不会覆盖数据里的哈?应该从插件里读取?未来?
|
||||||
.setConfigSchema(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription())
|
// .setConfigSchema(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription())
|
||||||
.setVersion(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getVersion())
|
// .setVersion(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getVersion())
|
||||||
.setDescription(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription());
|
// .setDescription(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription());
|
||||||
pluginConfigMapper.updateById(updatedPluginConfig);
|
// pluginConfigMapper.updateById(updatedPluginConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,13 +147,13 @@ public class IotPluginConfigServiceImpl implements IotPluginConfigService {
|
|||||||
*/
|
*/
|
||||||
private void validatePluginConfigFile(String pluginKeyNew) {
|
private void validatePluginConfigFile(String pluginKeyNew) {
|
||||||
// TODO @haohao:校验 file 相关参数,是否完整,类似:version 之类是不是可以解析到
|
// TODO @haohao:校验 file 相关参数,是否完整,类似:version 之类是不是可以解析到
|
||||||
PluginWrapper plugin = springPluginManager.getPlugin(pluginKeyNew);
|
// PluginWrapper plugin = springPluginManager.getPlugin(pluginKeyNew);
|
||||||
if (plugin == null) {
|
// if (plugin == null) {
|
||||||
throw exception(PLUGIN_INSTALL_FAILED);
|
// throw exception(PLUGIN_INSTALL_FAILED);
|
||||||
}
|
// }
|
||||||
if (plugin.getDescriptor().getVersion() == null) {
|
// if (plugin.getDescriptor().getVersion() == null) {
|
||||||
throw exception(PLUGIN_INSTALL_FAILED);
|
// throw exception(PLUGIN_INSTALL_FAILED);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package cn.iocoder.yudao.module.iot.service.plugin;
|
package cn.iocoder.yudao.module.iot.service.plugin;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.io.FileUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
|
||||||
@@ -9,13 +8,8 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO;
|
|||||||
import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO;
|
import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO;
|
||||||
import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginInstanceMapper;
|
import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginInstanceMapper;
|
||||||
import cn.iocoder.yudao.module.iot.dal.redis.plugin.DevicePluginProcessIdRedisDAO;
|
import cn.iocoder.yudao.module.iot.dal.redis.plugin.DevicePluginProcessIdRedisDAO;
|
||||||
import cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants;
|
|
||||||
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.pf4j.PluginState;
|
|
||||||
import org.pf4j.PluginWrapper;
|
|
||||||
import org.pf4j.spring.SpringPluginManager;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -23,17 +17,10 @@ import org.springframework.validation.annotation.Validated;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IoT 插件实例 Service 实现类
|
* IoT 插件实例 Service 实现类
|
||||||
*
|
*
|
||||||
@@ -54,8 +41,8 @@ public class IotPluginInstanceServiceImpl implements IotPluginInstanceService {
|
|||||||
@Resource
|
@Resource
|
||||||
private DevicePluginProcessIdRedisDAO devicePluginProcessIdRedisDAO;
|
private DevicePluginProcessIdRedisDAO devicePluginProcessIdRedisDAO;
|
||||||
|
|
||||||
@Resource
|
// @Resource
|
||||||
private SpringPluginManager pluginManager;
|
// private SpringPluginManager pluginManager;
|
||||||
|
|
||||||
@Value("${pf4j.pluginsDir}")
|
@Value("${pf4j.pluginsDir}")
|
||||||
private String pluginsDir;
|
private String pluginsDir;
|
||||||
@@ -120,17 +107,17 @@ public class IotPluginInstanceServiceImpl implements IotPluginInstanceService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stopAndUnloadPlugin(String pluginKey) {
|
public void stopAndUnloadPlugin(String pluginKey) {
|
||||||
PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
|
// PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
|
||||||
if (plugin == null) {
|
// if (plugin == null) {
|
||||||
log.warn("插件不存在或已卸载: {}", pluginKey);
|
// log.warn("插件不存在或已卸载: {}", pluginKey);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
if (plugin.getPluginState().equals(PluginState.STARTED)) {
|
// if (plugin.getPluginState().equals(PluginState.STARTED)) {
|
||||||
pluginManager.stopPlugin(pluginKey); // 停止插件
|
// pluginManager.stopPlugin(pluginKey); // 停止插件
|
||||||
log.info("已停止插件: {}", pluginKey);
|
// log.info("已停止插件: {}", pluginKey);
|
||||||
}
|
// }
|
||||||
pluginManager.unloadPlugin(pluginKey); // 卸载插件
|
// pluginManager.unloadPlugin(pluginKey); // 卸载插件
|
||||||
log.info("已卸载插件: {}", pluginKey);
|
// log.info("已卸载插件: {}", pluginKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -151,65 +138,66 @@ public class IotPluginInstanceServiceImpl implements IotPluginInstanceService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String uploadAndLoadNewPlugin(MultipartFile file) {
|
public String uploadAndLoadNewPlugin(MultipartFile file) {
|
||||||
String pluginKeyNew;
|
// String pluginKeyNew;
|
||||||
// TODO @haohao:多节点,是不是要上传 s3 之类的存储器;然后定时去加载
|
// // TODO @haohao:多节点,是不是要上传 s3 之类的存储器;然后定时去加载
|
||||||
Path pluginsPath = Paths.get(pluginsDir);
|
// Path pluginsPath = Paths.get(pluginsDir);
|
||||||
try {
|
// try {
|
||||||
FileUtil.mkdir(pluginsPath.toFile()); // 创建插件目录
|
// FileUtil.mkdir(pluginsPath.toFile()); // 创建插件目录
|
||||||
String filename = file.getOriginalFilename();
|
// String filename = file.getOriginalFilename();
|
||||||
if (filename != null) {
|
// if (filename != null) {
|
||||||
Path jarPath = pluginsPath.resolve(filename);
|
// Path jarPath = pluginsPath.resolve(filename);
|
||||||
Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件
|
// Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件
|
||||||
pluginKeyNew = pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件
|
//// pluginKeyNew = pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件
|
||||||
log.info("已加载插件: {}", pluginKeyNew);
|
//// log.info("已加载插件: {}", pluginKeyNew);
|
||||||
} else {
|
// } else {
|
||||||
throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED);
|
// throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED);
|
||||||
}
|
// }
|
||||||
} catch (IOException e) {
|
// } catch (IOException e) {
|
||||||
log.error("[uploadAndLoadNewPlugin][上传插件文件失败]", e);
|
// log.error("[uploadAndLoadNewPlugin][上传插件文件失败]", e);
|
||||||
throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e);
|
// throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e);
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
log.error("[uploadAndLoadNewPlugin][加载插件失败]", e);
|
// log.error("[uploadAndLoadNewPlugin][加载插件失败]", e);
|
||||||
throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e);
|
// throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e);
|
||||||
}
|
// }
|
||||||
return pluginKeyNew;
|
// return pluginKeyNew;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updatePluginStatus(IotPluginConfigDO pluginConfigDO, Integer status) {
|
public void updatePluginStatus(IotPluginConfigDO pluginConfigDO, Integer status) {
|
||||||
String pluginKey = pluginConfigDO.getPluginKey();
|
// String pluginKey = pluginConfigDO.getPluginKey();
|
||||||
PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
|
// PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
|
||||||
|
//
|
||||||
if (plugin == null) {
|
// if (plugin == null) {
|
||||||
// 插件不存在且状态为停止,抛出异常
|
// // 插件不存在且状态为停止,抛出异常
|
||||||
if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginConfigDO.getStatus())) {
|
// if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginConfigDO.getStatus())) {
|
||||||
throw exception(ErrorCodeConstants.PLUGIN_STATUS_INVALID);
|
// throw exception(ErrorCodeConstants.PLUGIN_STATUS_INVALID);
|
||||||
}
|
// }
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 启动插件
|
// // 启动插件
|
||||||
if (status.equals(IotPluginStatusEnum.RUNNING.getStatus())
|
// if (status.equals(IotPluginStatusEnum.RUNNING.getStatus())
|
||||||
&& plugin.getPluginState() != PluginState.STARTED) {
|
// && plugin.getPluginState() != PluginState.STARTED) {
|
||||||
try {
|
// try {
|
||||||
pluginManager.startPlugin(pluginKey);
|
// pluginManager.startPlugin(pluginKey);
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
log.error("[updatePluginStatus][启动插件({}) 失败]", pluginKey, e);
|
// log.error("[updatePluginStatus][启动插件({}) 失败]", pluginKey, e);
|
||||||
throw exception(ErrorCodeConstants.PLUGIN_START_FAILED, e);
|
// throw exception(ErrorCodeConstants.PLUGIN_START_FAILED, e);
|
||||||
}
|
// }
|
||||||
log.info("已启动插件: {}", pluginKey);
|
// log.info("已启动插件: {}", pluginKey);
|
||||||
}
|
// }
|
||||||
// 停止插件
|
// // 停止插件
|
||||||
else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus())
|
// else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus())
|
||||||
&& plugin.getPluginState() == PluginState.STARTED) {
|
// && plugin.getPluginState() == PluginState.STARTED) {
|
||||||
try {
|
// try {
|
||||||
pluginManager.stopPlugin(pluginKey);
|
// pluginManager.stopPlugin(pluginKey);
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
log.error("[updatePluginStatus][停止插件({}) 失败]", pluginKey, e);
|
// log.error("[updatePluginStatus][停止插件({}) 失败]", pluginKey, e);
|
||||||
throw exception(ErrorCodeConstants.PLUGIN_STOP_FAILED, e);
|
// throw exception(ErrorCodeConstants.PLUGIN_STOP_FAILED, e);
|
||||||
}
|
// }
|
||||||
log.info("已停止插件: {}", pluginKey);
|
// log.info("已停止插件: {}", pluginKey);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 设备与插件的映射操作 ==========
|
// ========== 设备与插件的映射操作 ==========
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.core.config;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamHandler;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamServer;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentInstanceHeartbeatJob;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentRegistry;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.upstream.IotDeviceUpstreamClient;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IoT 组件的通用自动配置类
|
|
||||||
*
|
|
||||||
* @author haohao
|
|
||||||
*/
|
|
||||||
@AutoConfiguration
|
|
||||||
@EnableConfigurationProperties(IotComponentCommonProperties.class)
|
|
||||||
@EnableScheduling // 开启定时任务,因为 IotComponentInstanceHeartbeatJob 是一个定时任务
|
|
||||||
public class IotComponentCommonAutoConfiguration {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 EMQX 设备下行服务器
|
|
||||||
*
|
|
||||||
* 当 yudao.iot.component.emqx.enabled = true 时,优先使用 emqxDeviceDownstreamHandler
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(prefix = "yudao.iot.component.emqx", name = "enabled", havingValue = "true")
|
|
||||||
public IotDeviceDownstreamServer emqxDeviceDownstreamServer(
|
|
||||||
IotComponentCommonProperties properties,
|
|
||||||
@Qualifier("emqxDeviceDownstreamHandler") IotDeviceDownstreamHandler deviceDownstreamHandler) {
|
|
||||||
return new IotDeviceDownstreamServer(properties, deviceDownstreamHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(initMethod = "init", destroyMethod = "stop")
|
|
||||||
public IotComponentInstanceHeartbeatJob pluginInstanceHeartbeatJob(IotDeviceUpstreamApi deviceUpstreamApi,
|
|
||||||
IotDeviceDownstreamServer deviceDownstreamServer,
|
|
||||||
IotComponentCommonProperties commonProperties,
|
|
||||||
IotComponentRegistry componentRegistry) {
|
|
||||||
return new IotComponentInstanceHeartbeatJob(deviceUpstreamApi, deviceDownstreamServer, commonProperties,
|
|
||||||
componentRegistry);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public IotDeviceUpstreamClient deviceUpstreamClient() {
|
|
||||||
return new IotDeviceUpstreamClient();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.core.config;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IoT 组件通用配置属性
|
|
||||||
*
|
|
||||||
* @author haohao
|
|
||||||
*/
|
|
||||||
@ConfigurationProperties(prefix = "yudao.iot.component.core")
|
|
||||||
@Validated
|
|
||||||
@Data
|
|
||||||
public class IotComponentCommonProperties {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 组件的唯一标识
|
|
||||||
* <p>
|
|
||||||
* 注意:该值将在运行时由各组件设置,不再从配置读取
|
|
||||||
*/
|
|
||||||
private String pluginKey;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
|
||||||
cn.iocoder.yudao.module.iot.component.core.config.IotPluginCommonAutoConfiguration
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
cn.iocoder.yudao.module.iot.component.core.config.IotComponentCommonAutoConfiguration
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.emqx.config;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.IdUtil;
|
|
||||||
import cn.hutool.system.SystemUtil;
|
|
||||||
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.config.IotComponentCommonProperties;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamHandler;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentRegistry;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.emqx.downstream.IotDeviceDownstreamHandlerImpl;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.emqx.upstream.IotDeviceUpstreamServer;
|
|
||||||
import io.vertx.core.Vertx;
|
|
||||||
import io.vertx.mqtt.MqttClient;
|
|
||||||
import io.vertx.mqtt.MqttClientOptions;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
|
|
||||||
import java.lang.management.ManagementFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IoT 组件 EMQX 的自动配置类
|
|
||||||
*
|
|
||||||
* @author haohao
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@AutoConfiguration
|
|
||||||
@EnableConfigurationProperties(IotComponentEmqxProperties.class)
|
|
||||||
@ConditionalOnProperty(prefix = "yudao.iot.component.emqx", name = "enabled", havingValue = "true", matchIfMissing = false)
|
|
||||||
// TODO @haohao:是不是不用扫 cn.iocoder.yudao.module.iot.component.core 拉,它尽量靠自动配置
|
|
||||||
@ComponentScan(basePackages = {
|
|
||||||
"cn.iocoder.yudao.module.iot.component.core", // 核心包
|
|
||||||
"cn.iocoder.yudao.module.iot.component.emqx" // EMQX 组件包
|
|
||||||
})
|
|
||||||
public class IotComponentEmqxAutoConfiguration {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 组件 key
|
|
||||||
*/
|
|
||||||
private static final String PLUGIN_KEY = "emqx";
|
|
||||||
|
|
||||||
public IotComponentEmqxAutoConfiguration() {
|
|
||||||
// TODO @haohao:这个日志,融合到 initialize ?
|
|
||||||
log.info("[IotComponentEmqxAutoConfiguration][已启动]");
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventListener(ApplicationStartedEvent.class)
|
|
||||||
public void initialize(ApplicationStartedEvent event) {
|
|
||||||
// 从应用上下文中获取需要的 Bean
|
|
||||||
IotComponentRegistry componentRegistry = event.getApplicationContext().getBean(IotComponentRegistry.class);
|
|
||||||
IotComponentCommonProperties commonProperties = event.getApplicationContext().getBean(IotComponentCommonProperties.class);
|
|
||||||
|
|
||||||
// 设置当前组件的核心标识
|
|
||||||
// TODO @haohao:如果多个组件,都去设置,会不会冲突哈?
|
|
||||||
commonProperties.setPluginKey(PLUGIN_KEY);
|
|
||||||
|
|
||||||
// 将 EMQX 组件注册到组件注册表
|
|
||||||
componentRegistry.registerComponent(
|
|
||||||
PLUGIN_KEY,
|
|
||||||
SystemUtil.getHostInfo().getAddress(),
|
|
||||||
0, // 内嵌模式固定为 0
|
|
||||||
getProcessId()
|
|
||||||
);
|
|
||||||
|
|
||||||
log.info("[initialize][IoT EMQX 组件初始化完成]");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @haohao:这个可能要注意,可能会有多个?冲突?
|
|
||||||
@Bean
|
|
||||||
public Vertx vertx() {
|
|
||||||
return Vertx.vertx();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public MqttClient mqttClient(Vertx vertx, IotComponentEmqxProperties emqxProperties) {
|
|
||||||
// TODO @haohao:这个日志,要不要去掉,避免过多哈
|
|
||||||
log.info("MQTT配置: host={}, port={}, username={}, ssl={}",
|
|
||||||
emqxProperties.getMqttHost(), emqxProperties.getMqttPort(),
|
|
||||||
emqxProperties.getMqttUsername(), emqxProperties.getMqttSsl());
|
|
||||||
|
|
||||||
MqttClientOptions options = new MqttClientOptions()
|
|
||||||
.setClientId("yudao-iot-downstream-" + IdUtil.fastSimpleUUID())
|
|
||||||
.setUsername(emqxProperties.getMqttUsername())
|
|
||||||
.setPassword(emqxProperties.getMqttPassword());
|
|
||||||
// TODO @haohao:可以用 ObjUtil.default
|
|
||||||
if (emqxProperties.getMqttSsl() != null) {
|
|
||||||
options.setSsl(emqxProperties.getMqttSsl());
|
|
||||||
} else {
|
|
||||||
options.setSsl(false);
|
|
||||||
}
|
|
||||||
return MqttClient.create(vertx, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "emqxDeviceUpstreamServer", initMethod = "start", destroyMethod = "stop")
|
|
||||||
public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi,
|
|
||||||
IotComponentEmqxProperties emqxProperties,
|
|
||||||
Vertx vertx,
|
|
||||||
MqttClient mqttClient,
|
|
||||||
IotComponentRegistry componentRegistry) {
|
|
||||||
return new IotDeviceUpstreamServer(emqxProperties, deviceUpstreamApi, vertx, mqttClient, componentRegistry);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "emqxDeviceDownstreamHandler")
|
|
||||||
public IotDeviceDownstreamHandler deviceDownstreamHandler(MqttClient mqttClient) {
|
|
||||||
return new IotDeviceDownstreamHandlerImpl(mqttClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @haohao:这个通用下一下哈。
|
|
||||||
/**
|
|
||||||
* 获取当前进程ID
|
|
||||||
*
|
|
||||||
* @return 进程ID
|
|
||||||
*/
|
|
||||||
private String getProcessId() {
|
|
||||||
// 获取进程的 name
|
|
||||||
String name = ManagementFactory.getRuntimeMXBean().getName();
|
|
||||||
// 分割名称,格式为 pid@hostname
|
|
||||||
String pid = name.split("@")[0];
|
|
||||||
return pid;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
cn.iocoder.yudao.module.iot.component.emqx.config.IotComponentEmqxAutoConfiguration
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.http.config;
|
|
||||||
|
|
||||||
import cn.hutool.system.SystemUtil;
|
|
||||||
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.config.IotComponentCommonProperties;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamHandler;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentRegistry;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.http.downstream.IotDeviceDownstreamHandlerImpl;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.http.upstream.IotDeviceUpstreamServer;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.ApplicationContext;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
|
|
||||||
import java.lang.management.ManagementFactory;
|
|
||||||
|
|
||||||
// TODO @haohao:类似 IotComponentEmqxAutoConfiguration 的建议
|
|
||||||
/**
|
|
||||||
* IoT 组件 HTTP 的自动配置类
|
|
||||||
*
|
|
||||||
* @author haohao
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@AutoConfiguration
|
|
||||||
@EnableConfigurationProperties(IotComponentHttpProperties.class)
|
|
||||||
@ConditionalOnProperty(prefix = "yudao.iot.component.http", name = "enabled", havingValue = "true", matchIfMissing = false)
|
|
||||||
@ComponentScan(basePackages = {
|
|
||||||
"cn.iocoder.yudao.module.iot.component.core", // 核心包
|
|
||||||
"cn.iocoder.yudao.module.iot.component.http" // HTTP组件包
|
|
||||||
})
|
|
||||||
public class IotComponentHttpAutoConfiguration {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 组件key
|
|
||||||
*/
|
|
||||||
private static final String PLUGIN_KEY = "http";
|
|
||||||
|
|
||||||
public IotComponentHttpAutoConfiguration() {
|
|
||||||
log.info("[IotComponentHttpAutoConfiguration][已启动]");
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventListener(ApplicationStartedEvent.class)
|
|
||||||
public void initialize(ApplicationStartedEvent event) {
|
|
||||||
// 从应用上下文中获取需要的Bean
|
|
||||||
IotComponentRegistry componentRegistry = event.getApplicationContext().getBean(IotComponentRegistry.class);
|
|
||||||
IotComponentCommonProperties commonProperties = event.getApplicationContext()
|
|
||||||
.getBean(IotComponentCommonProperties.class);
|
|
||||||
|
|
||||||
// 设置当前组件的核心标识
|
|
||||||
commonProperties.setPluginKey(PLUGIN_KEY);
|
|
||||||
|
|
||||||
// 将HTTP组件注册到组件注册表
|
|
||||||
componentRegistry.registerComponent(
|
|
||||||
PLUGIN_KEY,
|
|
||||||
SystemUtil.getHostInfo().getAddress(),
|
|
||||||
0, // 内嵌模式固定为0
|
|
||||||
getProcessId());
|
|
||||||
|
|
||||||
log.info("[initialize][IoT HTTP 组件初始化完成]");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "httpDeviceUpstreamServer", initMethod = "start", destroyMethod = "stop")
|
|
||||||
public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi,
|
|
||||||
IotComponentHttpProperties properties,
|
|
||||||
ApplicationContext applicationContext,
|
|
||||||
IotComponentRegistry componentRegistry) {
|
|
||||||
return new IotDeviceUpstreamServer(properties, deviceUpstreamApi, applicationContext, componentRegistry);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "httpDeviceDownstreamHandler")
|
|
||||||
public IotDeviceDownstreamHandler deviceDownstreamHandler() {
|
|
||||||
return new IotDeviceDownstreamHandlerImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前进程ID
|
|
||||||
*
|
|
||||||
* @return 进程ID
|
|
||||||
*/
|
|
||||||
private String getProcessId() {
|
|
||||||
// 获取进程的 name
|
|
||||||
String name = ManagementFactory.getRuntimeMXBean().getName();
|
|
||||||
// 分割名称,格式为 pid@hostname
|
|
||||||
String pid = name.split("@")[0];
|
|
||||||
return pid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.http.upstream;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentRegistry;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.http.config.IotComponentHttpProperties;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.http.upstream.router.IotDeviceUpstreamVertxHandler;
|
|
||||||
import io.vertx.core.Vertx;
|
|
||||||
import io.vertx.core.http.HttpServer;
|
|
||||||
import io.vertx.ext.web.Router;
|
|
||||||
import io.vertx.ext.web.handler.BodyHandler;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.context.ApplicationContext;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器
|
|
||||||
* <p>
|
|
||||||
* 协议:HTTP
|
|
||||||
*
|
|
||||||
* @author haohao
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class IotDeviceUpstreamServer {
|
|
||||||
|
|
||||||
private final Vertx vertx;
|
|
||||||
private final HttpServer server;
|
|
||||||
private final IotComponentHttpProperties properties;
|
|
||||||
private final IotComponentRegistry componentRegistry;
|
|
||||||
|
|
||||||
public IotDeviceUpstreamServer(IotComponentHttpProperties properties,
|
|
||||||
IotDeviceUpstreamApi deviceUpstreamApi,
|
|
||||||
ApplicationContext applicationContext,
|
|
||||||
IotComponentRegistry componentRegistry) {
|
|
||||||
this.properties = properties;
|
|
||||||
this.componentRegistry = componentRegistry;
|
|
||||||
|
|
||||||
// 创建 Vertx 实例
|
|
||||||
this.vertx = Vertx.vertx();
|
|
||||||
// 创建 Router 实例
|
|
||||||
Router router = Router.router(vertx);
|
|
||||||
router.route().handler(BodyHandler.create()); // 处理 Body
|
|
||||||
|
|
||||||
// 使用统一的 Handler 处理所有上行请求
|
|
||||||
IotDeviceUpstreamVertxHandler upstreamHandler = new IotDeviceUpstreamVertxHandler(deviceUpstreamApi,
|
|
||||||
applicationContext);
|
|
||||||
router.post(IotDeviceUpstreamVertxHandler.PROPERTY_PATH).handler(upstreamHandler);
|
|
||||||
router.post(IotDeviceUpstreamVertxHandler.EVENT_PATH).handler(upstreamHandler);
|
|
||||||
|
|
||||||
// 创建 HttpServer 实例
|
|
||||||
this.server = vertx.createHttpServer().requestHandler(router);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动 HTTP 服务器
|
|
||||||
*/
|
|
||||||
public void start() {
|
|
||||||
log.info("[start][开始启动]");
|
|
||||||
server.listen(properties.getServerPort())
|
|
||||||
.toCompletionStage()
|
|
||||||
.toCompletableFuture()
|
|
||||||
.join();
|
|
||||||
log.info("[start][启动完成,端口({})]", this.server.actualPort());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止所有
|
|
||||||
*/
|
|
||||||
public void stop() {
|
|
||||||
log.info("[stop][开始关闭]");
|
|
||||||
try {
|
|
||||||
// 关闭 HTTP 服务器
|
|
||||||
if (server != null) {
|
|
||||||
server.close()
|
|
||||||
.toCompletionStage()
|
|
||||||
.toCompletableFuture()
|
|
||||||
.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭 Vertx 实例
|
|
||||||
if (vertx != null) {
|
|
||||||
vertx.close()
|
|
||||||
.toCompletionStage()
|
|
||||||
.toCompletableFuture()
|
|
||||||
.join();
|
|
||||||
}
|
|
||||||
log.info("[stop][关闭完成]");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[stop][关闭异常]", e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.http.upstream.router;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.IdUtil;
|
|
||||||
import cn.hutool.core.util.ObjUtil;
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
|
||||||
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
|
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
|
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.pojo.IotStandardResponse;
|
|
||||||
import cn.iocoder.yudao.module.iot.component.core.util.IotPluginCommonUtils;
|
|
||||||
import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum;
|
|
||||||
import io.vertx.core.Handler;
|
|
||||||
import io.vertx.core.json.JsonObject;
|
|
||||||
import io.vertx.ext.web.RoutingContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.context.ApplicationContext;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
|
|
||||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IoT 设备上行统一处理的 Vert.x Handler
|
|
||||||
* <p>
|
|
||||||
* 统一处理设备属性上报和事件上报的请求
|
|
||||||
*
|
|
||||||
* @author haohao
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class IotDeviceUpstreamVertxHandler implements Handler<RoutingContext> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 属性上报路径
|
|
||||||
*/
|
|
||||||
public static final String PROPERTY_PATH = "/sys/:productKey/:deviceName/thing/event/property/post";
|
|
||||||
/**
|
|
||||||
* 事件上报路径
|
|
||||||
*/
|
|
||||||
public static final String EVENT_PATH = "/sys/:productKey/:deviceName/thing/event/:identifier/post";
|
|
||||||
|
|
||||||
private static final String PROPERTY_METHOD = "thing.event.property.post";
|
|
||||||
private static final String EVENT_METHOD_PREFIX = "thing.event.";
|
|
||||||
private static final String EVENT_METHOD_SUFFIX = ".post";
|
|
||||||
|
|
||||||
private final IotDeviceUpstreamApi deviceUpstreamApi;
|
|
||||||
// private final HttpScriptService scriptService;
|
|
||||||
|
|
||||||
public IotDeviceUpstreamVertxHandler(IotDeviceUpstreamApi deviceUpstreamApi,
|
|
||||||
ApplicationContext applicationContext) {
|
|
||||||
this.deviceUpstreamApi = deviceUpstreamApi;
|
|
||||||
// this.scriptService = applicationContext.getBean(HttpScriptService.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handle(RoutingContext routingContext) {
|
|
||||||
String path = routingContext.request().path();
|
|
||||||
String requestId = IdUtil.fastSimpleUUID();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 解析通用参数
|
|
||||||
String productKey = routingContext.pathParam("productKey");
|
|
||||||
String deviceName = routingContext.pathParam("deviceName");
|
|
||||||
JsonObject body = routingContext.body().asJsonObject();
|
|
||||||
requestId = ObjUtil.defaultIfBlank(body.getString("id"), requestId);
|
|
||||||
|
|
||||||
// 2. 根据路径模式处理不同类型的请求
|
|
||||||
CommonResult<Boolean> result;
|
|
||||||
String method;
|
|
||||||
if (path.matches(".*/thing/event/property/post")) {
|
|
||||||
// 处理属性上报
|
|
||||||
IotDevicePropertyReportReqDTO reportReqDTO = parsePropertyReportRequest(productKey, deviceName,
|
|
||||||
requestId, body);
|
|
||||||
|
|
||||||
// 设备上线
|
|
||||||
updateDeviceState(reportReqDTO.getProductKey(), reportReqDTO.getDeviceName());
|
|
||||||
|
|
||||||
// 属性上报
|
|
||||||
result = deviceUpstreamApi.reportDeviceProperty(reportReqDTO);
|
|
||||||
method = PROPERTY_METHOD;
|
|
||||||
} else if (path.matches(".*/thing/event/.+/post")) {
|
|
||||||
// 处理事件上报
|
|
||||||
String identifier = routingContext.pathParam("identifier");
|
|
||||||
IotDeviceEventReportReqDTO reportReqDTO = parseEventReportRequest(productKey, deviceName, identifier,
|
|
||||||
requestId, body);
|
|
||||||
|
|
||||||
// 设备上线
|
|
||||||
updateDeviceState(reportReqDTO.getProductKey(), reportReqDTO.getDeviceName());
|
|
||||||
|
|
||||||
// 事件上报
|
|
||||||
result = deviceUpstreamApi.reportDeviceEvent(reportReqDTO);
|
|
||||||
method = EVENT_METHOD_PREFIX + identifier + EVENT_METHOD_SUFFIX;
|
|
||||||
} else {
|
|
||||||
// 不支持的请求路径
|
|
||||||
IotStandardResponse errorResponse = IotStandardResponse.error(requestId, "unknown",
|
|
||||||
BAD_REQUEST.getCode(), "不支持的请求路径");
|
|
||||||
IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 返回标准响应
|
|
||||||
IotStandardResponse response;
|
|
||||||
if (result.isSuccess()) {
|
|
||||||
response = IotStandardResponse.success(requestId, method, result.getData());
|
|
||||||
} else {
|
|
||||||
response = IotStandardResponse.error(requestId, method, result.getCode(), result.getMsg());
|
|
||||||
}
|
|
||||||
IotPluginCommonUtils.writeJsonResponse(routingContext, response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[handle][处理上行请求异常] path={}", path, e);
|
|
||||||
String method = path.contains("/property/") ? PROPERTY_METHOD
|
|
||||||
: EVENT_METHOD_PREFIX + (routingContext.pathParams().containsKey("identifier")
|
|
||||||
? routingContext.pathParam("identifier")
|
|
||||||
: "unknown") + EVENT_METHOD_SUFFIX;
|
|
||||||
IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method,
|
|
||||||
INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
|
|
||||||
IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新设备状态
|
|
||||||
*
|
|
||||||
* @param productKey 产品 Key
|
|
||||||
* @param deviceName 设备名称
|
|
||||||
*/
|
|
||||||
private void updateDeviceState(String productKey, String deviceName) {
|
|
||||||
deviceUpstreamApi.updateDeviceState(((IotDeviceStateUpdateReqDTO) new IotDeviceStateUpdateReqDTO()
|
|
||||||
.setRequestId(IdUtil.fastSimpleUUID()).setProcessId(IotPluginCommonUtils.getProcessId())
|
|
||||||
.setReportTime(LocalDateTime.now())
|
|
||||||
.setProductKey(productKey).setDeviceName(deviceName)).setState(IotDeviceStateEnum.ONLINE.getState()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析属性上报请求
|
|
||||||
*
|
|
||||||
* @param productKey 产品 Key
|
|
||||||
* @param deviceName 设备名称
|
|
||||||
* @param requestId 请求 ID
|
|
||||||
* @param body 请求体
|
|
||||||
* @return 属性上报请求 DTO
|
|
||||||
*/
|
|
||||||
private IotDevicePropertyReportReqDTO parsePropertyReportRequest(String productKey, String deviceName,
|
|
||||||
String requestId, JsonObject body) {
|
|
||||||
// 使用脚本解析数据
|
|
||||||
// Map<String, Object> properties = scriptService.parsePropertyData(productKey, deviceName, body);
|
|
||||||
|
|
||||||
|
|
||||||
// 如果脚本解析结果为空,使用默认解析逻辑
|
|
||||||
// TODO @芋艿:注释说明一下,为什么要这么处理?
|
|
||||||
// if (CollUtil.isNotEmpty(properties)) {
|
|
||||||
Map<String, Object> properties = new HashMap<>();
|
|
||||||
Map<String, Object> params = body.getJsonObject("params") != null ?
|
|
||||||
body.getJsonObject("params").getMap() : null;
|
|
||||||
if (params != null) {
|
|
||||||
// 将标准格式的 params 转换为平台需要的 properties 格式
|
|
||||||
for (Map.Entry<String, Object> entry : params.entrySet()) {
|
|
||||||
String key = entry.getKey();
|
|
||||||
Object valueObj = entry.getValue();
|
|
||||||
// 如果是复杂结构(包含 value 和 time)
|
|
||||||
if (valueObj instanceof Map) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> valueMap = (Map<String, Object>) valueObj;
|
|
||||||
properties.put(key, valueMap.getOrDefault("value", valueObj));
|
|
||||||
} else {
|
|
||||||
properties.put(key, valueObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 构建属性上报请求 DTO
|
|
||||||
return ((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO().setRequestId(requestId)
|
|
||||||
.setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now())
|
|
||||||
.setProductKey(productKey).setDeviceName(deviceName)).setProperties(properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析事件上报请求
|
|
||||||
*
|
|
||||||
* @param productKey 产品K ey
|
|
||||||
* @param deviceName 设备名称
|
|
||||||
* @param identifier 事件标识符
|
|
||||||
* @param requestId 请求 ID
|
|
||||||
* @param body 请求体
|
|
||||||
* @return 事件上报请求 DTO
|
|
||||||
*/
|
|
||||||
private IotDeviceEventReportReqDTO parseEventReportRequest(String productKey, String deviceName, String identifier,
|
|
||||||
String requestId, JsonObject body) {
|
|
||||||
// 使用脚本解析事件数据
|
|
||||||
// Map<String, Object> params = scriptService.parseEventData(productKey, deviceName, identifier, body);
|
|
||||||
Map<String, Object> params = null;
|
|
||||||
|
|
||||||
// 如果脚本解析结果为空,使用默认解析逻辑
|
|
||||||
// if (CollUtil.isNotEmpty(params)) {
|
|
||||||
if (body.containsKey("params")) {
|
|
||||||
params = body.getJsonObject("params").getMap();
|
|
||||||
} else {
|
|
||||||
// 兼容旧格式
|
|
||||||
params = new HashMap<>();
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 构建事件上报请求 DTO
|
|
||||||
return ((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO().setRequestId(requestId)
|
|
||||||
.setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now())
|
|
||||||
.setProductKey(productKey).setDeviceName(deviceName)).setIdentifier(identifier).setParams(params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
cn.iocoder.yudao.module.iot.component.http.config.IotComponentHttpAutoConfiguration
|
|
||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
该模块包含多个 IoT 设备连接组件,提供不同的通信协议支持:
|
该模块包含多个 IoT 设备连接组件,提供不同的通信协议支持:
|
||||||
|
|
||||||
- `yudao-module-iot-component-core`: 核心接口和通用类
|
- `yudao-module-iot-net-component-core`: 核心接口和通用类
|
||||||
- `yudao-module-iot-component-http`: 基于 HTTP 协议的设备通信组件
|
- `yudao-module-iot-net-component-http`: 基于 HTTP 协议的设备通信组件
|
||||||
- `yudao-module-iot-component-emqx`: 基于 MQTT/EMQX 的设备通信组件
|
- `yudao-module-iot-net-component-emqx`: 基于 MQTT/EMQX 的设备通信组件
|
||||||
|
|
||||||
## 组件架构
|
## 组件架构
|
||||||
|
|
||||||
@@ -9,18 +9,18 @@
|
|||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<artifactId>yudao-module-iot-components</artifactId>
|
<artifactId>yudao-module-iot-net-components</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
<name>${project.artifactId}</name>
|
<name>${project.artifactId}</name>
|
||||||
<description>
|
<description>
|
||||||
物联网组件模块,提供与物联网设备通讯、管理的组件实现
|
物联网网络组件模块,提供与物联网设备通讯、管理的网络组件实现
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>yudao-module-iot-component-core</module>
|
<module>yudao-module-iot-net-component-core</module>
|
||||||
<module>yudao-module-iot-component-http</module>
|
<module>yudao-module-iot-net-component-http</module>
|
||||||
<module>yudao-module-iot-component-emqx</module>
|
<module>yudao-module-iot-net-component-emqx</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
@@ -3,19 +3,18 @@
|
|||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<parent>
|
<parent>
|
||||||
<artifactId>yudao-module-iot-components</artifactId>
|
<artifactId>yudao-module-iot-net-components</artifactId>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<artifactId>yudao-module-iot-component-core</artifactId>
|
<artifactId>yudao-module-iot-net-component-core</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>${project.artifactId}</name>
|
<name>${project.artifactId}</name>
|
||||||
<!-- TODO @芋艿:description 后续统一优化一波 -->
|
|
||||||
<description>
|
<description>
|
||||||
物联网组件核心模块
|
物联网网络组件核心模块
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.net.component.core.config;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.downstream.IotDeviceDownstreamHandler;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.downstream.IotDeviceDownstreamServer;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.heartbeat.IotNetComponentInstanceHeartbeatJob;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.heartbeat.IotNetComponentRegistry;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.upstream.IotDeviceUpstreamClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT 网络组件的通用自动配置类
|
||||||
|
*
|
||||||
|
* @author haohao
|
||||||
|
*/
|
||||||
|
@AutoConfiguration
|
||||||
|
@EnableConfigurationProperties(IotNetComponentCommonProperties.class)
|
||||||
|
@EnableScheduling // 开启定时任务,因为 IotNetComponentInstanceHeartbeatJob 是一个定时任务
|
||||||
|
public class IotNetComponentCommonAutoConfiguration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 EMQX 设备下行服务器
|
||||||
|
* <p>
|
||||||
|
* 当 yudao.iot.component.emqx.enabled = true 时,优先使用 emqxDeviceDownstreamHandler
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(prefix = "yudao.iot.component.emqx", name = "enabled", havingValue = "true")
|
||||||
|
public IotDeviceDownstreamServer emqxDeviceDownstreamServer(
|
||||||
|
IotNetComponentCommonProperties properties,
|
||||||
|
@Qualifier("emqxDeviceDownstreamHandler") IotDeviceDownstreamHandler deviceDownstreamHandler) {
|
||||||
|
return new IotDeviceDownstreamServer(properties, deviceDownstreamHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建网络组件实例心跳任务
|
||||||
|
*/
|
||||||
|
@Bean(initMethod = "init", destroyMethod = "stop")
|
||||||
|
public IotNetComponentInstanceHeartbeatJob pluginInstanceHeartbeatJob(
|
||||||
|
IotDeviceUpstreamApi deviceUpstreamApi,
|
||||||
|
IotNetComponentCommonProperties commonProperties,
|
||||||
|
IotNetComponentRegistry componentRegistry) {
|
||||||
|
return new IotNetComponentInstanceHeartbeatJob(
|
||||||
|
deviceUpstreamApi,
|
||||||
|
commonProperties,
|
||||||
|
componentRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建设备上行客户端
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public IotDeviceUpstreamClient deviceUpstreamClient() {
|
||||||
|
return new IotDeviceUpstreamClient();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.net.component.core.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT 网络组件通用配置属性
|
||||||
|
*
|
||||||
|
* @author haohao
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties(prefix = "yudao.iot.component")
|
||||||
|
@Validated
|
||||||
|
@Data
|
||||||
|
public class IotNetComponentCommonProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件的唯一标识
|
||||||
|
* <p>
|
||||||
|
* 注意:该值将在运行时由各组件设置,不再从配置读取
|
||||||
|
*/
|
||||||
|
private String pluginKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件实例心跳超时时间,单位:毫秒
|
||||||
|
* <p>
|
||||||
|
* 默认值:30 秒
|
||||||
|
*/
|
||||||
|
private Long instanceHeartbeatTimeout = 30000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网络组件消息转发配置
|
||||||
|
*/
|
||||||
|
private ForwardMessage forwardMessage = new ForwardMessage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息转发配置
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class ForwardMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否转发所有设备消息到 EMQX
|
||||||
|
* <p>
|
||||||
|
* 默认为 true 开启
|
||||||
|
*/
|
||||||
|
private boolean forwardAllDeviceMessageToEmqx = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否转发所有设备消息到 HTTP
|
||||||
|
* <p>
|
||||||
|
* 默认为 false 关闭
|
||||||
|
*/
|
||||||
|
private boolean forwardAllDeviceMessageToHttp = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.net.component.core.constants;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT 设备主题枚举
|
||||||
|
* <p>
|
||||||
|
* 用于统一管理 MQTT 协议中的主题常量,基于 Alink 协议规范
|
||||||
|
*
|
||||||
|
* @author haohao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum IotDeviceTopicEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统主题前缀
|
||||||
|
*/
|
||||||
|
SYS_TOPIC_PREFIX("/sys/", "系统主题前缀"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务调用主题前缀
|
||||||
|
*/
|
||||||
|
SERVICE_TOPIC_PREFIX("/thing/service/", "服务调用主题前缀"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备属性设置主题
|
||||||
|
* 请求Topic:/sys/${productKey}/${deviceName}/thing/service/property/set
|
||||||
|
* 响应Topic:/sys/${productKey}/${deviceName}/thing/service/property/set_reply
|
||||||
|
*/
|
||||||
|
PROPERTY_SET_TOPIC("/thing/service/property/set", "设备属性设置主题"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备属性获取主题
|
||||||
|
* 请求Topic:/sys/${productKey}/${deviceName}/thing/service/property/get
|
||||||
|
* 响应Topic:/sys/${productKey}/${deviceName}/thing/service/property/get_reply
|
||||||
|
*/
|
||||||
|
PROPERTY_GET_TOPIC("/thing/service/property/get", "设备属性获取主题"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备配置设置主题
|
||||||
|
* 请求Topic:/sys/${productKey}/${deviceName}/thing/service/config/set
|
||||||
|
* 响应Topic:/sys/${productKey}/${deviceName}/thing/service/config/set_reply
|
||||||
|
*/
|
||||||
|
CONFIG_SET_TOPIC("/thing/service/config/set", "设备配置设置主题"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备OTA升级主题
|
||||||
|
* 请求Topic:/sys/${productKey}/${deviceName}/thing/service/ota/upgrade
|
||||||
|
* 响应Topic:/sys/${productKey}/${deviceName}/thing/service/ota/upgrade_reply
|
||||||
|
*/
|
||||||
|
OTA_UPGRADE_TOPIC("/thing/service/ota/upgrade", "设备OTA升级主题"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备属性上报主题
|
||||||
|
* 请求Topic:/sys/${productKey}/${deviceName}/thing/event/property/post
|
||||||
|
* 响应Topic:/sys/${productKey}/${deviceName}/thing/event/property/post_reply
|
||||||
|
*/
|
||||||
|
PROPERTY_POST_TOPIC("/thing/event/property/post", "设备属性上报主题"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备事件上报主题前缀
|
||||||
|
*/
|
||||||
|
EVENT_POST_TOPIC_PREFIX("/thing/event/", "设备事件上报主题前缀"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备事件上报主题后缀
|
||||||
|
*/
|
||||||
|
EVENT_POST_TOPIC_SUFFIX("/post", "设备事件上报主题后缀"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应主题后缀
|
||||||
|
*/
|
||||||
|
REPLY_SUFFIX("_reply", "响应主题后缀");
|
||||||
|
|
||||||
|
private final String topic;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
IotDeviceTopicEnum(String topic, String description) {
|
||||||
|
this.topic = topic;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建设备服务调用主题
|
||||||
|
*
|
||||||
|
* @param productKey 产品Key
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
* @param serviceIdentifier 服务标识符
|
||||||
|
* @return 完整的主题路径
|
||||||
|
*/
|
||||||
|
public static String buildServiceTopic(String productKey, String deviceName, String serviceIdentifier) {
|
||||||
|
return SYS_TOPIC_PREFIX.getTopic() + productKey + "/" + deviceName +
|
||||||
|
SERVICE_TOPIC_PREFIX.getTopic() + serviceIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建设备属性设置主题
|
||||||
|
*
|
||||||
|
* @param productKey 产品Key
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
* @return 完整的主题路径
|
||||||
|
*/
|
||||||
|
public static String buildPropertySetTopic(String productKey, String deviceName) {
|
||||||
|
return SYS_TOPIC_PREFIX.getTopic() + productKey + "/" + deviceName + PROPERTY_SET_TOPIC.getTopic();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建设备属性获取主题
|
||||||
|
*
|
||||||
|
* @param productKey 产品Key
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
* @return 完整的主题路径
|
||||||
|
*/
|
||||||
|
public static String buildPropertyGetTopic(String productKey, String deviceName) {
|
||||||
|
return SYS_TOPIC_PREFIX.getTopic() + productKey + "/" + deviceName + PROPERTY_GET_TOPIC.getTopic();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建设备配置设置主题
|
||||||
|
*
|
||||||
|
* @param productKey 产品Key
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
* @return 完整的主题路径
|
||||||
|
*/
|
||||||
|
public static String buildConfigSetTopic(String productKey, String deviceName) {
|
||||||
|
return SYS_TOPIC_PREFIX.getTopic() + productKey + "/" + deviceName + CONFIG_SET_TOPIC.getTopic();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建设备OTA升级主题
|
||||||
|
*
|
||||||
|
* @param productKey 产品Key
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
* @return 完整的主题路径
|
||||||
|
*/
|
||||||
|
public static String buildOtaUpgradeTopic(String productKey, String deviceName) {
|
||||||
|
return SYS_TOPIC_PREFIX.getTopic() + productKey + "/" + deviceName + OTA_UPGRADE_TOPIC.getTopic();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建设备属性上报主题
|
||||||
|
*
|
||||||
|
* @param productKey 产品Key
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
* @return 完整的主题路径
|
||||||
|
*/
|
||||||
|
public static String buildPropertyPostTopic(String productKey, String deviceName) {
|
||||||
|
return SYS_TOPIC_PREFIX.getTopic() + productKey + "/" + deviceName + PROPERTY_POST_TOPIC.getTopic();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建设备事件上报主题
|
||||||
|
*
|
||||||
|
* @param productKey 产品Key
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
* @param eventIdentifier 事件标识符
|
||||||
|
* @return 完整的主题路径
|
||||||
|
*/
|
||||||
|
public static String buildEventPostTopic(String productKey, String deviceName, String eventIdentifier) {
|
||||||
|
return SYS_TOPIC_PREFIX.getTopic() + productKey + "/" + deviceName +
|
||||||
|
EVENT_POST_TOPIC_PREFIX.getTopic() + eventIdentifier + EVENT_POST_TOPIC_SUFFIX.getTopic();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取响应主题
|
||||||
|
*
|
||||||
|
* @param requestTopic 请求主题
|
||||||
|
* @return 响应主题
|
||||||
|
*/
|
||||||
|
public static String getReplyTopic(String requestTopic) {
|
||||||
|
return requestTopic + REPLY_SUFFIX.getTopic();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.core.downstream;
|
package cn.iocoder.yudao.module.iot.net.component.core.downstream;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.core.downstream;
|
package cn.iocoder.yudao.module.iot.net.component.core.downstream;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
|
||||||
import cn.iocoder.yudao.module.iot.component.core.config.IotComponentCommonProperties;
|
import cn.iocoder.yudao.module.iot.net.component.core.config.IotNetComponentCommonProperties;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class IotDeviceDownstreamServer {
|
public class IotDeviceDownstreamServer {
|
||||||
|
|
||||||
private final IotComponentCommonProperties properties;
|
private final IotNetComponentCommonProperties properties;
|
||||||
private final IotDeviceDownstreamHandler deviceDownstreamHandler;
|
private final IotDeviceDownstreamHandler deviceDownstreamHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,59 +1,49 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.core.heartbeat;
|
package cn.iocoder.yudao.module.iot.net.component.core.heartbeat;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.system.SystemUtil;
|
import cn.hutool.system.SystemUtil;
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
|
||||||
import cn.iocoder.yudao.module.iot.component.core.config.IotComponentCommonProperties;
|
import cn.iocoder.yudao.module.iot.net.component.core.config.IotNetComponentCommonProperties;
|
||||||
import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamServer;
|
import cn.iocoder.yudao.module.iot.net.component.core.heartbeat.IotNetComponentRegistry.IotNetComponentInfo;
|
||||||
import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentRegistry.IotComponentInfo;
|
import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
|
||||||
import java.lang.management.ManagementFactory;
|
import java.util.Collection;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IoT 组件实例心跳定时任务
|
* IoT 网络组件实例心跳定时任务
|
||||||
* <p>
|
* <p>
|
||||||
* 将组件的状态,定时上报给 server 服务器
|
* 将组件的状态,定时上报给 server 服务器
|
||||||
|
*
|
||||||
|
* @author haohao
|
||||||
*/
|
*/
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class IotComponentInstanceHeartbeatJob {
|
public class IotNetComponentInstanceHeartbeatJob {
|
||||||
|
|
||||||
/**
|
|
||||||
* 内嵌模式的端口值(固定为 0)
|
|
||||||
*/
|
|
||||||
private static final Integer EMBEDDED_PORT = 0;
|
|
||||||
|
|
||||||
private final IotDeviceUpstreamApi deviceUpstreamApi;
|
private final IotDeviceUpstreamApi deviceUpstreamApi;
|
||||||
private final IotDeviceDownstreamServer deviceDownstreamServer; // TODO @haohao:这个变量还需要哇?
|
private final IotNetComponentCommonProperties commonProperties;
|
||||||
private final IotComponentCommonProperties commonProperties;
|
private final IotNetComponentRegistry componentRegistry;
|
||||||
private final IotComponentRegistry componentRegistry;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化方法,由 Spring调 用:注册当前组件并发送上线心跳
|
* 初始化方法,由 Spring 调用:注册当前组件并发送上线心跳
|
||||||
*/
|
*/
|
||||||
public void init() {
|
public void init() {
|
||||||
// 将当前组件注册到注册表
|
|
||||||
String processId = getProcessId();
|
|
||||||
String hostIp = SystemUtil.getHostInfo().getAddress();
|
|
||||||
|
|
||||||
// 注册当前组件
|
|
||||||
componentRegistry.registerComponent(
|
|
||||||
commonProperties.getPluginKey(),
|
|
||||||
hostIp,
|
|
||||||
EMBEDDED_PORT,
|
|
||||||
processId);
|
|
||||||
|
|
||||||
// 发送所有组件的上线心跳
|
// 发送所有组件的上线心跳
|
||||||
for (IotComponentInfo component : componentRegistry.getAllComponents()) {
|
Collection<IotNetComponentInfo> components = componentRegistry.getAllComponents();
|
||||||
|
if (CollUtil.isEmpty(components)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (IotNetComponentInfo component : components) {
|
||||||
try {
|
try {
|
||||||
CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(
|
CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(
|
||||||
buildPluginInstanceHeartbeatReqDTO(component, true));
|
buildPluginInstanceHeartbeatReqDTO(component, true));
|
||||||
log.info("[init][组件({})上线结果:{})]", component.getPluginKey(), result);
|
log.info("[init][组件({})上线结果:{}]", component.getPluginKey(), result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[init][组件({})上线发送异常]", component.getPluginKey(), e);
|
log.error("[init][组件({})上线发送异常]", component.getPluginKey(), e);
|
||||||
}
|
}
|
||||||
@@ -65,11 +55,15 @@ public class IotComponentInstanceHeartbeatJob {
|
|||||||
*/
|
*/
|
||||||
public void stop() {
|
public void stop() {
|
||||||
// 发送所有组件的下线心跳
|
// 发送所有组件的下线心跳
|
||||||
for (IotComponentInfo component : componentRegistry.getAllComponents()) {
|
Collection<IotNetComponentInfo> components = componentRegistry.getAllComponents();
|
||||||
|
if (CollUtil.isEmpty(components)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (IotNetComponentInfo component : components) {
|
||||||
try {
|
try {
|
||||||
CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(
|
CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(
|
||||||
buildPluginInstanceHeartbeatReqDTO(component, false));
|
buildPluginInstanceHeartbeatReqDTO(component, false));
|
||||||
log.info("[stop][组件({})下线结果:{})]", component.getPluginKey(), result);
|
log.info("[stop][组件({})下线结果:{}]", component.getPluginKey(), result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[stop][组件({})下线发送异常]", component.getPluginKey(), e);
|
log.error("[stop][组件({})下线发送异常]", component.getPluginKey(), e);
|
||||||
}
|
}
|
||||||
@@ -85,11 +79,15 @@ public class IotComponentInstanceHeartbeatJob {
|
|||||||
@Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.MINUTES) // 1 分钟执行一次
|
@Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.MINUTES) // 1 分钟执行一次
|
||||||
public void execute() {
|
public void execute() {
|
||||||
// 发送所有组件的心跳
|
// 发送所有组件的心跳
|
||||||
for (IotComponentInfo component : componentRegistry.getAllComponents()) {
|
Collection<IotNetComponentInfo> components = componentRegistry.getAllComponents();
|
||||||
|
if (CollUtil.isEmpty(components)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (IotNetComponentInfo component : components) {
|
||||||
try {
|
try {
|
||||||
CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(
|
CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(
|
||||||
buildPluginInstanceHeartbeatReqDTO(component, true));
|
buildPluginInstanceHeartbeatReqDTO(component, true));
|
||||||
log.info("[execute][组件({})心跳结果:{})]", component.getPluginKey(), result);
|
log.info("[execute][组件({})心跳结果:{}]", component.getPluginKey(), result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[execute][组件({})心跳发送异常]", component.getPluginKey(), e);
|
log.error("[execute][组件({})心跳发送异常]", component.getPluginKey(), e);
|
||||||
}
|
}
|
||||||
@@ -103,23 +101,11 @@ public class IotComponentInstanceHeartbeatJob {
|
|||||||
* @param online 是否在线
|
* @param online 是否在线
|
||||||
* @return 心跳 DTO
|
* @return 心跳 DTO
|
||||||
*/
|
*/
|
||||||
private IotPluginInstanceHeartbeatReqDTO buildPluginInstanceHeartbeatReqDTO(IotComponentInfo component,
|
private IotPluginInstanceHeartbeatReqDTO buildPluginInstanceHeartbeatReqDTO(IotNetComponentInfo component,
|
||||||
Boolean online) {
|
Boolean online) {
|
||||||
return new IotPluginInstanceHeartbeatReqDTO()
|
return new IotPluginInstanceHeartbeatReqDTO()
|
||||||
.setPluginKey(component.getPluginKey()).setProcessId(component.getProcessId())
|
.setPluginKey(component.getPluginKey()).setProcessId(component.getProcessId())
|
||||||
.setHostIp(component.getHostIp()).setDownstreamPort(component.getDownstreamPort())
|
.setHostIp(component.getHostIp()).setDownstreamPort(component.getDownstreamPort())
|
||||||
.setOnline(online);
|
.setOnline(online);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// TODO @haohao:要和 IotPluginCommonUtils 保持一致么?
|
|
||||||
/**
|
|
||||||
* 获取当前进程 ID
|
|
||||||
*
|
|
||||||
* @return 进程 ID
|
|
||||||
*/
|
|
||||||
private String getProcessId() {
|
|
||||||
String name = ManagementFactory.getRuntimeMXBean().getName();
|
|
||||||
// TODO @haohao:是不是 SystemUtil.getCurrentPID(); 直接获取 pid 哈?
|
|
||||||
return name.split("@")[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.core.heartbeat;
|
package cn.iocoder.yudao.module.iot.net.component.core.heartbeat;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.map.MapUtil;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -8,49 +10,51 @@ import java.util.Collection;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
// TODO @haohao:组件相关的注释,要不把 组件 => 网络组件?可能更容易理解?
|
|
||||||
// TODO @haohao:yudao-module-iot-components => yudao-module-iot-net-components 增加一个 net 如何?虽然会长一点,但是意思更精准?
|
|
||||||
/**
|
/**
|
||||||
* IoT 组件注册表
|
* IoT 网络组件注册表
|
||||||
* <p>
|
* <p>
|
||||||
* 用于管理多个组件的注册信息,解决多组件心跳问题
|
* 用于管理多个网络组件的注册信息,解决多组件心跳问题
|
||||||
|
*
|
||||||
|
* @author haohao
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class IotComponentRegistry {
|
public class IotNetComponentRegistry {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件信息
|
* 网络组件信息
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public static class IotComponentInfo {
|
public static class IotNetComponentInfo {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件 Key
|
* 组件 Key
|
||||||
*/
|
*/
|
||||||
private final String pluginKey;
|
private final String pluginKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主机 IP
|
* 主机 IP
|
||||||
*/
|
*/
|
||||||
private final String hostIp;
|
private final String hostIp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下游端口
|
* 下游端口
|
||||||
*/
|
*/
|
||||||
private final Integer downstreamPort;
|
private final Integer downstreamPort;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 进程 ID
|
* 进程 ID
|
||||||
*/
|
*/
|
||||||
private final String processId;
|
private final String processId;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件映射表:key 为组件 Key
|
* 组件映射表:key 为组件 Key
|
||||||
*/
|
*/
|
||||||
private final Map<String, IotComponentInfo> components = new ConcurrentHashMap<>();
|
private final Map<String, IotNetComponentInfo> components = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册组件
|
* 注册网络组件
|
||||||
*
|
*
|
||||||
* @param pluginKey 组件 Key
|
* @param pluginKey 组件 Key
|
||||||
* @param hostIp 主机 IP
|
* @param hostIp 主机 IP
|
||||||
@@ -58,38 +62,37 @@ public class IotComponentRegistry {
|
|||||||
* @param processId 进程 ID
|
* @param processId 进程 ID
|
||||||
*/
|
*/
|
||||||
public void registerComponent(String pluginKey, String hostIp, Integer downstreamPort, String processId) {
|
public void registerComponent(String pluginKey, String hostIp, Integer downstreamPort, String processId) {
|
||||||
log.info("[registerComponent][注册组件, pluginKey={}, hostIp={}, downstreamPort={}, processId={}]",
|
log.info("[registerComponent][注册网络组件, pluginKey={}, hostIp={}, downstreamPort={}, processId={}]",
|
||||||
pluginKey, hostIp, downstreamPort, processId);
|
pluginKey, hostIp, downstreamPort, processId);
|
||||||
components.put(pluginKey, new IotComponentInfo(pluginKey, hostIp, downstreamPort, processId));
|
components.put(pluginKey, new IotNetComponentInfo(pluginKey, hostIp, downstreamPort, processId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注销组件
|
* 注销网络组件
|
||||||
*
|
*
|
||||||
* @param pluginKey 组件 Key
|
* @param pluginKey 组件 Key
|
||||||
*/
|
*/
|
||||||
public void unregisterComponent(String pluginKey) {
|
public void unregisterComponent(String pluginKey) {
|
||||||
log.info("[unregisterComponent][注销组件, pluginKey={}]", pluginKey);
|
log.info("[unregisterComponent][注销网络组件, pluginKey={}]", pluginKey);
|
||||||
components.remove(pluginKey);
|
components.remove(pluginKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有组件
|
* 获取所有网络组件
|
||||||
*
|
*
|
||||||
* @return 所有组件集合
|
* @return 所有组件集合
|
||||||
*/
|
*/
|
||||||
public Collection<IotComponentInfo> getAllComponents() {
|
public Collection<IotNetComponentInfo> getAllComponents() {
|
||||||
return components.values();
|
return CollUtil.isEmpty(components) ? CollUtil.newArrayList() : components.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定组件
|
* 获取指定网络组件
|
||||||
*
|
*
|
||||||
* @param pluginKey 组件 Key
|
* @param pluginKey 组件 Key
|
||||||
* @return 组件信息
|
* @return 组件信息
|
||||||
*/
|
*/
|
||||||
public IotComponentInfo getComponent(String pluginKey) {
|
public IotNetComponentInfo getComponent(String pluginKey) {
|
||||||
return components.get(pluginKey);
|
return MapUtil.isEmpty(components) ? null : components.get(pluginKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.net.component.core.message;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.IdUtil;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT Alink 消息模型
|
||||||
|
* <p>
|
||||||
|
* 基于阿里云 Alink 协议规范实现的标准消息格式
|
||||||
|
*
|
||||||
|
* @author haohao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class IotAlinkMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息 ID
|
||||||
|
*/
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 协议版本
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private String version = "1.0";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息方法
|
||||||
|
*/
|
||||||
|
private String method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息参数
|
||||||
|
*/
|
||||||
|
private Map<String, Object> params;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为 JSONObject
|
||||||
|
*
|
||||||
|
* @return JSONObject 对象
|
||||||
|
*/
|
||||||
|
public JSONObject toJsonObject() {
|
||||||
|
JSONObject json = new JSONObject();
|
||||||
|
json.set("id", id);
|
||||||
|
json.set("version", version);
|
||||||
|
json.set("method", method);
|
||||||
|
json.set("params", params != null ? params : new JSONObject());
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为 JSON 字符串
|
||||||
|
*
|
||||||
|
* @return JSON 字符串
|
||||||
|
*/
|
||||||
|
public String toJsonString() {
|
||||||
|
return toJsonObject().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建设备服务调用消息
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID,为空时自动生成
|
||||||
|
* @param serviceIdentifier 服务标识符
|
||||||
|
* @param params 服务参数
|
||||||
|
* @return Alink 消息对象
|
||||||
|
*/
|
||||||
|
public static IotAlinkMessage createServiceInvokeMessage(String requestId, String serviceIdentifier,
|
||||||
|
Map<String, Object> params) {
|
||||||
|
return IotAlinkMessage.builder()
|
||||||
|
.id(requestId != null ? requestId : generateRequestId())
|
||||||
|
.method("thing.service." + serviceIdentifier)
|
||||||
|
.params(params)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建设备属性设置消息
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID,为空时自动生成
|
||||||
|
* @param properties 设备属性
|
||||||
|
* @return Alink 消息对象
|
||||||
|
*/
|
||||||
|
public static IotAlinkMessage createPropertySetMessage(String requestId, Map<String, Object> properties) {
|
||||||
|
return IotAlinkMessage.builder()
|
||||||
|
.id(requestId != null ? requestId : generateRequestId())
|
||||||
|
.method("thing.service.property.set")
|
||||||
|
.params(properties)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建设备属性获取消息
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID,为空时自动生成
|
||||||
|
* @param identifiers 要获取的属性标识符列表
|
||||||
|
* @return Alink 消息对象
|
||||||
|
*/
|
||||||
|
public static IotAlinkMessage createPropertyGetMessage(String requestId, String[] identifiers) {
|
||||||
|
JSONObject params = new JSONObject();
|
||||||
|
params.set("identifiers", identifiers);
|
||||||
|
|
||||||
|
return IotAlinkMessage.builder()
|
||||||
|
.id(requestId != null ? requestId : generateRequestId())
|
||||||
|
.method("thing.service.property.get")
|
||||||
|
.params(params)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建设备配置设置消息
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID,为空时自动生成
|
||||||
|
* @param configs 设备配置
|
||||||
|
* @return Alink 消息对象
|
||||||
|
*/
|
||||||
|
public static IotAlinkMessage createConfigSetMessage(String requestId, Map<String, Object> configs) {
|
||||||
|
return IotAlinkMessage.builder()
|
||||||
|
.id(requestId != null ? requestId : generateRequestId())
|
||||||
|
.method("thing.service.config.set")
|
||||||
|
.params(configs)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建设备 OTA 升级消息
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID,为空时自动生成
|
||||||
|
* @param otaInfo OTA 升级信息
|
||||||
|
* @return Alink 消息对象
|
||||||
|
*/
|
||||||
|
public static IotAlinkMessage createOtaUpgradeMessage(String requestId, Map<String, Object> otaInfo) {
|
||||||
|
return IotAlinkMessage.builder()
|
||||||
|
.id(requestId != null ? requestId : generateRequestId())
|
||||||
|
.method("thing.service.ota.upgrade")
|
||||||
|
.params(otaInfo)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成请求 ID
|
||||||
|
*
|
||||||
|
* @return 请求 ID
|
||||||
|
*/
|
||||||
|
public static String generateRequestId() {
|
||||||
|
return IdUtil.fastSimpleUUID();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.core.pojo;
|
package cn.iocoder.yudao.module.iot.net.component.core.pojo;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IoT 标准协议响应实体类
|
* IoT 标准协议响应实体类
|
||||||
@@ -10,10 +12,11 @@ import lombok.Data;
|
|||||||
* @author haohao
|
* @author haohao
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
public class IotStandardResponse {
|
public class IotStandardResponse {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息ID
|
* 消息 ID
|
||||||
*/
|
*/
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
@@ -45,7 +48,7 @@ public class IotStandardResponse {
|
|||||||
/**
|
/**
|
||||||
* 创建成功响应
|
* 创建成功响应
|
||||||
*
|
*
|
||||||
* @param id 消息ID
|
* @param id 消息 ID
|
||||||
* @param method 方法名
|
* @param method 方法名
|
||||||
* @return 成功响应
|
* @return 成功响应
|
||||||
*/
|
*/
|
||||||
@@ -56,28 +59,37 @@ public class IotStandardResponse {
|
|||||||
/**
|
/**
|
||||||
* 创建成功响应
|
* 创建成功响应
|
||||||
*
|
*
|
||||||
* @param id 消息ID
|
* @param id 消息 ID
|
||||||
* @param method 方法名
|
* @param method 方法名
|
||||||
* @param data 响应数据
|
* @param data 响应数据
|
||||||
* @return 成功响应
|
* @return 成功响应
|
||||||
*/
|
*/
|
||||||
public static IotStandardResponse success(String id, String method, Object data) {
|
public static IotStandardResponse success(String id, String method, Object data) {
|
||||||
return new IotStandardResponse().setId(id).setCode(200).setData(data).setMessage("success")
|
return new IotStandardResponse()
|
||||||
.setMethod(method).setVersion("1.0");
|
.setId(id)
|
||||||
|
.setCode(200)
|
||||||
|
.setData(data)
|
||||||
|
.setMessage("success")
|
||||||
|
.setMethod(method)
|
||||||
|
.setVersion("1.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建错误响应
|
* 创建错误响应
|
||||||
*
|
*
|
||||||
* @param id 消息ID
|
* @param id 消息 ID
|
||||||
* @param method 方法名
|
* @param method 方法名
|
||||||
* @param code 错误码
|
* @param code 错误码
|
||||||
* @param message 错误消息
|
* @param message 错误消息
|
||||||
* @return 错误响应
|
* @return 错误响应
|
||||||
*/
|
*/
|
||||||
public static IotStandardResponse error(String id, String method, Integer code, String message) {
|
public static IotStandardResponse error(String id, String method, Integer code, String message) {
|
||||||
return new IotStandardResponse().setId(id).setCode(code).setData(null).setMessage(message)
|
return new IotStandardResponse()
|
||||||
.setMethod(method).setVersion("1.0");
|
.setId(id)
|
||||||
|
.setCode(code)
|
||||||
|
.setData(null)
|
||||||
|
.setMessage(StrUtil.blankToDefault(message, "error"))
|
||||||
|
.setMethod(method)
|
||||||
|
.setVersion("1.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.core.upstream;
|
package cn.iocoder.yudao.module.iot.net.component.core.upstream;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
||||||
@@ -1,27 +1,31 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.core.util;
|
package cn.iocoder.yudao.module.iot.net.component.core.util;
|
||||||
|
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.system.SystemUtil;
|
import cn.hutool.system.SystemUtil;
|
||||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||||
import cn.iocoder.yudao.module.iot.component.core.pojo.IotStandardResponse;
|
import cn.iocoder.yudao.module.iot.net.component.core.pojo.IotStandardResponse;
|
||||||
import io.vertx.core.http.HttpHeaders;
|
import io.vertx.core.http.HttpHeaders;
|
||||||
import io.vertx.ext.web.RoutingContext;
|
import io.vertx.ext.web.RoutingContext;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
// TODO @haohao:名字要改下哈。
|
|
||||||
/**
|
/**
|
||||||
* IoT 插件的通用工具类
|
* IoT 网络组件的通用工具类
|
||||||
*
|
*
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
public class IotPluginCommonUtils {
|
public class IotNetComponentCommonUtils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程实例的进程编号
|
* 流程实例的进程编号
|
||||||
*/
|
*/
|
||||||
private static String processId;
|
private static String processId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取进程ID
|
||||||
|
*
|
||||||
|
* @return 进程ID
|
||||||
|
*/
|
||||||
public static String getProcessId() {
|
public static String getProcessId() {
|
||||||
if (StrUtil.isEmpty(processId)) {
|
if (StrUtil.isEmpty(processId)) {
|
||||||
initProcessId();
|
initProcessId();
|
||||||
@@ -29,11 +33,23 @@ public class IotPluginCommonUtils {
|
|||||||
return processId;
|
return processId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化进程ID
|
||||||
|
*/
|
||||||
private synchronized static void initProcessId() {
|
private synchronized static void initProcessId() {
|
||||||
processId = String.format("%s@%d@%s", // IP@PID@${uuid}
|
processId = String.format("%s@%d@%s", // IP@PID@${uuid}
|
||||||
SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID(), IdUtil.fastSimpleUUID());
|
SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID(), IdUtil.fastSimpleUUID());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成请求ID
|
||||||
|
*
|
||||||
|
* @return 生成的唯一请求ID
|
||||||
|
*/
|
||||||
|
public static String generateRequestId() {
|
||||||
|
return IdUtil.fastSimpleUUID();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将对象转换为JSON字符串后写入HTTP响应
|
* 将对象转换为JSON字符串后写入HTTP响应
|
||||||
*
|
*
|
||||||
@@ -51,20 +67,20 @@ public class IotPluginCommonUtils {
|
|||||||
/**
|
/**
|
||||||
* 生成标准JSON格式的响应并写入HTTP响应(基于IotStandardResponse)
|
* 生成标准JSON格式的响应并写入HTTP响应(基于IotStandardResponse)
|
||||||
* <p>
|
* <p>
|
||||||
* 推荐使用此方法,统一MQTT和HTTP的响应格式。使用方式:
|
* 推荐使用此方法,统一 MQTT 和 HTTP 的响应格式。使用方式:
|
||||||
*
|
*
|
||||||
* <pre>
|
* <pre>
|
||||||
* // 成功响应
|
* // 成功响应
|
||||||
* IotStandardResponse response = IotStandardResponse.success(requestId, method, data);
|
* IotStandardResponse response = IotStandardResponse.success(requestId, method, data);
|
||||||
* IotPluginCommonUtils.writeJsonResponse(routingContext, response);
|
* IotNetComponentCommonUtils.writeJsonResponse(routingContext, response);
|
||||||
*
|
*
|
||||||
* // 错误响应
|
* // 错误响应
|
||||||
* IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, code, message);
|
* IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, code, message);
|
||||||
* IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse);
|
* IotNetComponentCommonUtils.writeJsonResponse(routingContext, errorResponse);
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* @param routingContext 路由上下文
|
* @param routingContext 路由上下文
|
||||||
* @param response IotStandardResponse响应对象
|
* @param response IotStandardResponse 响应对象
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public static void writeJsonResponse(RoutingContext routingContext, IotStandardResponse response) {
|
public static void writeJsonResponse(RoutingContext routingContext, IotStandardResponse response) {
|
||||||
@@ -73,5 +89,4 @@ public class IotPluginCommonUtils {
|
|||||||
.putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
|
.putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||||
.end(JsonUtils.toJsonString(response));
|
.end(JsonUtils.toJsonString(response));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||||
|
cn.iocoder.yudao.module.iot.net.component.core.config.IotNetComponentCommonAutoConfiguration
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
cn.iocoder.yudao.module.iot.net.component.core.config.IotNetComponentCommonAutoConfiguration
|
||||||
@@ -3,23 +3,23 @@
|
|||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<parent>
|
<parent>
|
||||||
<artifactId>yudao-module-iot-components</artifactId>
|
<artifactId>yudao-module-iot-net-components</artifactId>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<artifactId>yudao-module-iot-component-emqx</artifactId>
|
<artifactId>yudao-module-iot-net-component-emqx</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>${project.artifactId}</name>
|
<name>${project.artifactId}</name>
|
||||||
<description>
|
<description>
|
||||||
物联网组件 EMQX 模块
|
物联网网络组件 EMQX 模块
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-module-iot-component-core</artifactId>
|
<artifactId>yudao-module-iot-net-component-core</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.net.component.emqx.config;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.IdUtil;
|
||||||
|
import cn.hutool.core.util.ObjUtil;
|
||||||
|
import cn.hutool.system.SystemUtil;
|
||||||
|
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.config.IotNetComponentCommonProperties;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.downstream.IotDeviceDownstreamHandler;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.heartbeat.IotNetComponentRegistry;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.emqx.downstream.IotDeviceDownstreamHandlerImpl;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.emqx.upstream.IotDeviceUpstreamServer;
|
||||||
|
import io.vertx.core.Vertx;
|
||||||
|
import io.vertx.mqtt.MqttClient;
|
||||||
|
import io.vertx.mqtt.MqttClientOptions;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT 网络组件 EMQX 的自动配置类
|
||||||
|
*
|
||||||
|
* @author haohao
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@AutoConfiguration
|
||||||
|
@EnableConfigurationProperties(IotNetComponentEmqxProperties.class)
|
||||||
|
@ConditionalOnProperty(prefix = "yudao.iot.component.emqx", name = "enabled", havingValue = "true", matchIfMissing = false)
|
||||||
|
@ComponentScan(basePackages = {
|
||||||
|
"cn.iocoder.yudao.module.iot.net.component.emqx" // 只扫描 EMQX 组件包
|
||||||
|
})
|
||||||
|
public class IotNetComponentEmqxAutoConfiguration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件 key
|
||||||
|
*/
|
||||||
|
private static final String PLUGIN_KEY = "emqx";
|
||||||
|
|
||||||
|
public IotNetComponentEmqxAutoConfiguration() {
|
||||||
|
// 构造函数中不输出日志,移到 initialize 方法中
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 EMQX 组件
|
||||||
|
*
|
||||||
|
* @param event 应用启动事件
|
||||||
|
*/
|
||||||
|
@EventListener(ApplicationStartedEvent.class)
|
||||||
|
public void initialize(ApplicationStartedEvent event) {
|
||||||
|
log.info("[IotNetComponentEmqxAutoConfiguration][开始初始化]");
|
||||||
|
|
||||||
|
// 从应用上下文中获取需要的 Bean
|
||||||
|
IotNetComponentRegistry componentRegistry = event.getApplicationContext()
|
||||||
|
.getBean(IotNetComponentRegistry.class);
|
||||||
|
IotNetComponentCommonProperties commonProperties = event.getApplicationContext()
|
||||||
|
.getBean(IotNetComponentCommonProperties.class);
|
||||||
|
|
||||||
|
// 设置当前组件的核心标识
|
||||||
|
// 注意:这里只为当前 EMQX 组件设置 pluginKey,不影响其他组件
|
||||||
|
commonProperties.setPluginKey(PLUGIN_KEY);
|
||||||
|
|
||||||
|
// 将 EMQX 组件注册到组件注册表
|
||||||
|
componentRegistry.registerComponent(
|
||||||
|
PLUGIN_KEY,
|
||||||
|
SystemUtil.getHostInfo().getAddress(),
|
||||||
|
0, // 内嵌模式固定为 0
|
||||||
|
IotNetComponentCommonUtils.getProcessId());
|
||||||
|
|
||||||
|
log.info("[initialize][IoT EMQX 组件初始化完成]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Vert.x 实例
|
||||||
|
*/
|
||||||
|
@Bean(name = "emqxVertx")
|
||||||
|
public Vertx vertx() {
|
||||||
|
return Vertx.vertx();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 MQTT 客户端
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public MqttClient mqttClient(@Qualifier("emqxVertx") Vertx vertx, IotNetComponentEmqxProperties emqxProperties) {
|
||||||
|
// 使用 debug 级别记录详细配置,减少生产环境日志
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("MQTT 配置: host={}, port={}, username={}, ssl={}",
|
||||||
|
emqxProperties.getMqttHost(), emqxProperties.getMqttPort(),
|
||||||
|
emqxProperties.getMqttUsername(), emqxProperties.getMqttSsl());
|
||||||
|
} else {
|
||||||
|
log.info("MQTT 连接至: {}:{}", emqxProperties.getMqttHost(), emqxProperties.getMqttPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
MqttClientOptions options = new MqttClientOptions()
|
||||||
|
.setClientId("yudao-iot-downstream-" + IdUtil.fastSimpleUUID())
|
||||||
|
.setUsername(emqxProperties.getMqttUsername())
|
||||||
|
.setPassword(emqxProperties.getMqttPassword());
|
||||||
|
// 设置 SSL 选项
|
||||||
|
options.setSsl(ObjUtil.defaultIfNull(emqxProperties.getMqttSsl(), false));
|
||||||
|
return MqttClient.create(vertx, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建设备上行服务器
|
||||||
|
*/
|
||||||
|
@Bean(name = "emqxDeviceUpstreamServer", initMethod = "start", destroyMethod = "stop")
|
||||||
|
public IotDeviceUpstreamServer deviceUpstreamServer(
|
||||||
|
IotDeviceUpstreamApi deviceUpstreamApi,
|
||||||
|
IotNetComponentEmqxProperties emqxProperties,
|
||||||
|
@Qualifier("emqxVertx") Vertx vertx,
|
||||||
|
MqttClient mqttClient,
|
||||||
|
IotNetComponentRegistry componentRegistry) {
|
||||||
|
return new IotDeviceUpstreamServer(emqxProperties, deviceUpstreamApi, vertx, mqttClient, componentRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建设备下行处理器
|
||||||
|
*/
|
||||||
|
@Bean(name = "emqxDeviceDownstreamHandler")
|
||||||
|
public IotDeviceDownstreamHandler deviceDownstreamHandler(MqttClient mqttClient) {
|
||||||
|
return new IotDeviceDownstreamHandlerImpl(mqttClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,44 +1,51 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.emqx.config;
|
package cn.iocoder.yudao.module.iot.net.component.emqx.config;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IoT EMQX 组件配置属性
|
* IoT EMQX 网络组件配置属性
|
||||||
|
*
|
||||||
|
* @author haohao
|
||||||
*/
|
*/
|
||||||
@ConfigurationProperties(prefix = "yudao.iot.component.emqx")
|
@ConfigurationProperties(prefix = "yudao.iot.component.emqx")
|
||||||
@Data
|
@Data
|
||||||
public class IotComponentEmqxProperties {
|
@Validated
|
||||||
|
public class IotNetComponentEmqxProperties {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否启用 EMQX 组件
|
* 是否启用 EMQX 组件
|
||||||
*/
|
*/
|
||||||
private Boolean enabled;
|
private Boolean enabled;
|
||||||
|
|
||||||
// TODO @haohao:一般中英文之间,加个空格哈,写作(注释)习惯。类似 MQTT 密码;
|
|
||||||
/**
|
/**
|
||||||
* 服务主机
|
* MQTT 服务主机
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "MQTT 服务器主机不能为空")
|
@NotBlank(message = "MQTT 服务器主机不能为空")
|
||||||
private String mqttHost;
|
private String mqttHost;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务端口
|
* MQTT 服务端口
|
||||||
*/
|
*/
|
||||||
@NotNull(message = "MQTT 服务器端口不能为空")
|
@NotNull(message = "MQTT 服务器端口不能为空")
|
||||||
private Integer mqttPort;
|
private Integer mqttPort;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务用户名
|
* MQTT 服务用户名
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "MQTT 服务器用户名不能为空")
|
@NotBlank(message = "MQTT 服务器用户名不能为空")
|
||||||
private String mqttUsername;
|
private String mqttUsername;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务密码
|
* MQTT 服务密码
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "MQTT 服务器密码不能为空")
|
@NotBlank(message = "MQTT 服务器密码不能为空")
|
||||||
private String mqttPassword;
|
private String mqttPassword;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否启用 SSL
|
* 是否启用 SSL
|
||||||
*/
|
*/
|
||||||
@@ -57,4 +64,17 @@ public class IotComponentEmqxProperties {
|
|||||||
@NotNull(message = "认证端口不能为空")
|
@NotNull(message = "认证端口不能为空")
|
||||||
private Integer authPort;
|
private Integer authPort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重连延迟时间(毫秒)
|
||||||
|
* <p>
|
||||||
|
* 默认值:5000 毫秒
|
||||||
|
*/
|
||||||
|
private Integer reconnectDelayMs = 5000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接超时时间(毫秒)
|
||||||
|
* <p>
|
||||||
|
* 默认值:10000 毫秒
|
||||||
|
*/
|
||||||
|
private Integer connectionTimeoutMs = 10000;
|
||||||
}
|
}
|
||||||
@@ -1,48 +1,38 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.emqx.downstream;
|
package cn.iocoder.yudao.module.iot.net.component.emqx.downstream;
|
||||||
|
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONObject;
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
|
||||||
import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamHandler;
|
import cn.iocoder.yudao.module.iot.net.component.core.constants.IotDeviceTopicEnum;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.downstream.IotDeviceDownstreamHandler;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.message.IotAlinkMessage;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils;
|
||||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||||
import io.vertx.core.buffer.Buffer;
|
import io.vertx.core.buffer.Buffer;
|
||||||
import io.vertx.mqtt.MqttClient;
|
import io.vertx.mqtt.MqttClient;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.MQTT_TOPIC_ILLEGAL;
|
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.MQTT_TOPIC_ILLEGAL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EMQX 插件的 {@link IotDeviceDownstreamHandler} 实现类
|
* EMQX 网络组件的 {@link IotDeviceDownstreamHandler} 实现类
|
||||||
*
|
*
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler {
|
public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler {
|
||||||
|
|
||||||
private static final String SYS_TOPIC_PREFIX = "/sys/";
|
/**
|
||||||
|
* MQTT 客户端
|
||||||
// TODO @haohao:是不是可以类似 IotDeviceConfigSetVertxHandler 的建议,抽到统一的枚举类
|
*/
|
||||||
// TODO @haohao:讨论,感觉 mqtt 和 http,可以做个相对统一的格式哈。;回复 都使用 Alink 格式,方便后续扩展。
|
|
||||||
// 设备服务调用 标准 JSON
|
|
||||||
// 请求Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}
|
|
||||||
// 响应Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}_reply
|
|
||||||
private static final String SERVICE_TOPIC_PREFIX = "/thing/service/";
|
|
||||||
|
|
||||||
// 设置设备属性 标准 JSON
|
|
||||||
// 请求Topic:/sys/${productKey}/${deviceName}/thing/service/property/set
|
|
||||||
// 响应Topic:/sys/${productKey}/${deviceName}/thing/service/property/set_reply
|
|
||||||
private static final String PROPERTY_SET_TOPIC = "/thing/service/property/set";
|
|
||||||
|
|
||||||
private final MqttClient mqttClient;
|
private final MqttClient mqttClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构造函数
|
* 构造函数
|
||||||
*
|
*
|
||||||
* @param mqttClient MQTT客户端
|
* @param mqttClient MQTT 客户端
|
||||||
*/
|
*/
|
||||||
public IotDeviceDownstreamHandlerImpl(MqttClient mqttClient) {
|
public IotDeviceDownstreamHandlerImpl(MqttClient mqttClient) {
|
||||||
this.mqttClient = mqttClient;
|
this.mqttClient = mqttClient;
|
||||||
@@ -60,12 +50,17 @@ public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandle
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 构建请求主题
|
// 构建请求主题
|
||||||
String topic = buildServiceTopic(reqDTO.getProductKey(), reqDTO.getDeviceName(), reqDTO.getIdentifier());
|
String topic = IotDeviceTopicEnum.buildServiceTopic(reqDTO.getProductKey(), reqDTO.getDeviceName(),
|
||||||
|
reqDTO.getIdentifier());
|
||||||
|
|
||||||
// 构建请求消息
|
// 构建请求消息
|
||||||
String requestId = reqDTO.getRequestId() != null ? reqDTO.getRequestId() : generateRequestId();
|
String requestId = StrUtil.isNotEmpty(reqDTO.getRequestId()) ? reqDTO.getRequestId()
|
||||||
JSONObject request = buildServiceRequest(requestId, reqDTO.getIdentifier(), reqDTO.getParams());
|
: IotNetComponentCommonUtils.generateRequestId();
|
||||||
|
IotAlinkMessage message = IotAlinkMessage.createServiceInvokeMessage(
|
||||||
|
requestId, reqDTO.getIdentifier(), reqDTO.getParams());
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
publishMessage(topic, request);
|
publishMessage(topic, message.toJsonObject());
|
||||||
|
|
||||||
log.info("[invokeService][调用设备服务成功][requestId: {}][topic: {}]", requestId, topic);
|
log.info("[invokeService][调用设备服务成功][requestId: {}][topic: {}]", requestId, topic);
|
||||||
return CommonResult.success(true);
|
return CommonResult.success(true);
|
||||||
@@ -77,13 +72,15 @@ public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandle
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResult<Boolean> getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) {
|
public CommonResult<Boolean> getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) {
|
||||||
|
// 暂未实现,返回成功
|
||||||
return CommonResult.success(true);
|
return CommonResult.success(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResult<Boolean> setDeviceProperty(IotDevicePropertySetReqDTO reqDTO) {
|
public CommonResult<Boolean> setDeviceProperty(IotDevicePropertySetReqDTO reqDTO) {
|
||||||
// 验证参数
|
|
||||||
log.info("[setProperty][开始设置设备属性][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
|
log.info("[setProperty][开始设置设备属性][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
|
||||||
|
|
||||||
|
// 验证参数
|
||||||
if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null) {
|
if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null) {
|
||||||
log.error("[setProperty][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
|
log.error("[setProperty][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
|
||||||
return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
|
return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
|
||||||
@@ -91,12 +88,15 @@ public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandle
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 构建请求主题
|
// 构建请求主题
|
||||||
String topic = buildPropertySetTopic(reqDTO.getProductKey(), reqDTO.getDeviceName());
|
String topic = IotDeviceTopicEnum.buildPropertySetTopic(reqDTO.getProductKey(), reqDTO.getDeviceName());
|
||||||
|
|
||||||
// 构建请求消息
|
// 构建请求消息
|
||||||
String requestId = reqDTO.getRequestId() != null ? reqDTO.getRequestId() : generateRequestId();
|
String requestId = StrUtil.isNotEmpty(reqDTO.getRequestId()) ? reqDTO.getRequestId()
|
||||||
JSONObject request = buildPropertySetRequest(requestId, reqDTO.getProperties());
|
: IotNetComponentCommonUtils.generateRequestId();
|
||||||
|
IotAlinkMessage message = IotAlinkMessage.createPropertySetMessage(requestId, reqDTO.getProperties());
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
publishMessage(topic, request);
|
publishMessage(topic, message.toJsonObject());
|
||||||
|
|
||||||
log.info("[setProperty][设置设备属性成功][requestId: {}][topic: {}]", requestId, topic);
|
log.info("[setProperty][设置设备属性成功][requestId: {}][topic: {}]", requestId, topic);
|
||||||
return CommonResult.success(true);
|
return CommonResult.success(true);
|
||||||
@@ -108,54 +108,21 @@ public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandle
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResult<Boolean> setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) {
|
public CommonResult<Boolean> setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) {
|
||||||
|
// 暂未实现,返回成功
|
||||||
return CommonResult.success(true);
|
return CommonResult.success(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResult<Boolean> upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) {
|
public CommonResult<Boolean> upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) {
|
||||||
|
// 暂未实现,返回成功
|
||||||
return CommonResult.success(true);
|
return CommonResult.success(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建服务调用主题
|
|
||||||
*/
|
|
||||||
private String buildServiceTopic(String productKey, String deviceName, String serviceIdentifier) {
|
|
||||||
return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + SERVICE_TOPIC_PREFIX + serviceIdentifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建属性设置主题
|
|
||||||
*/
|
|
||||||
private String buildPropertySetTopic(String productKey, String deviceName) {
|
|
||||||
return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + PROPERTY_SET_TOPIC;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @haohao:这个,后面搞个对象,会不会好点哈?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建服务调用请求
|
|
||||||
*/
|
|
||||||
private JSONObject buildServiceRequest(String requestId, String serviceIdentifier, Map<String, Object> params) {
|
|
||||||
return new JSONObject()
|
|
||||||
.set("id", requestId)
|
|
||||||
.set("version", "1.0")
|
|
||||||
.set("method", "thing.service." + serviceIdentifier)
|
|
||||||
.set("params", params != null ? params : new JSONObject());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建属性设置请求
|
|
||||||
*/
|
|
||||||
private JSONObject buildPropertySetRequest(String requestId, Map<String, Object> properties) {
|
|
||||||
return new JSONObject()
|
|
||||||
.set("id", requestId)
|
|
||||||
.set("version", "1.0")
|
|
||||||
.set("method", "thing.service.property.set")
|
|
||||||
.set("params", properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发布 MQTT 消息
|
* 发布 MQTT 消息
|
||||||
|
*
|
||||||
|
* @param topic 主题
|
||||||
|
* @param payload 消息内容
|
||||||
*/
|
*/
|
||||||
private void publishMessage(String topic, JSONObject payload) {
|
private void publishMessage(String topic, JSONObject payload) {
|
||||||
mqttClient.publish(
|
mqttClient.publish(
|
||||||
@@ -166,13 +133,4 @@ public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandle
|
|||||||
false);
|
false);
|
||||||
log.info("[publishMessage][发送消息成功][topic: {}][payload: {}]", topic, payload);
|
log.info("[publishMessage][发送消息成功][topic: {}][payload: {}]", topic, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @haohao:这个要不抽到 IotPluginCommonUtils 里?
|
|
||||||
/**
|
|
||||||
* 生成请求 ID
|
|
||||||
*/
|
|
||||||
private String generateRequestId() {
|
|
||||||
return IdUtil.fastSimpleUUID();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.emqx.upstream;
|
package cn.iocoder.yudao.module.iot.net.component.emqx.upstream;
|
||||||
|
|
||||||
import cn.hutool.core.util.ArrayUtil;
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
||||||
import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentRegistry;
|
import cn.iocoder.yudao.module.iot.net.component.core.heartbeat.IotNetComponentRegistry;
|
||||||
import cn.iocoder.yudao.module.iot.component.emqx.config.IotComponentEmqxProperties;
|
import cn.iocoder.yudao.module.iot.net.component.emqx.config.IotNetComponentEmqxProperties;
|
||||||
import cn.iocoder.yudao.module.iot.component.emqx.upstream.router.IotDeviceAuthVertxHandler;
|
import cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router.IotDeviceAuthVertxHandler;
|
||||||
import cn.iocoder.yudao.module.iot.component.emqx.upstream.router.IotDeviceMqttMessageHandler;
|
import cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router.IotDeviceMqttMessageHandler;
|
||||||
import cn.iocoder.yudao.module.iot.component.emqx.upstream.router.IotDeviceWebhookVertxHandler;
|
import cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router.IotDeviceWebhookVertxHandler;
|
||||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.Vertx;
|
import io.vertx.core.Vertx;
|
||||||
@@ -21,7 +21,7 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器
|
* IoT 设备上行服务端,接收来自 device 设备的请求,转发给 server 服务器
|
||||||
* <p>
|
* <p>
|
||||||
* 协议:HTTP、MQTT
|
* 协议:HTTP、MQTT
|
||||||
*
|
*
|
||||||
@@ -30,15 +30,6 @@ import java.util.concurrent.TimeUnit;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class IotDeviceUpstreamServer {
|
public class IotDeviceUpstreamServer {
|
||||||
|
|
||||||
// TODO @haohao:抽到 IotComponentEmqxProperties 里?
|
|
||||||
/**
|
|
||||||
* 重连延迟时间(毫秒)
|
|
||||||
*/
|
|
||||||
private static final int RECONNECT_DELAY_MS = 5000;
|
|
||||||
/**
|
|
||||||
* 连接超时时间(毫秒)
|
|
||||||
*/
|
|
||||||
private static final int CONNECTION_TIMEOUT_MS = 10000;
|
|
||||||
/**
|
/**
|
||||||
* 默认 QoS 级别
|
* 默认 QoS 级别
|
||||||
*/
|
*/
|
||||||
@@ -47,20 +38,20 @@ public class IotDeviceUpstreamServer {
|
|||||||
private final Vertx vertx;
|
private final Vertx vertx;
|
||||||
private final HttpServer server;
|
private final HttpServer server;
|
||||||
private final MqttClient client;
|
private final MqttClient client;
|
||||||
private final IotComponentEmqxProperties emqxProperties;
|
private final IotNetComponentEmqxProperties emqxProperties;
|
||||||
private final IotDeviceMqttMessageHandler mqttMessageHandler;
|
private final IotDeviceMqttMessageHandler mqttMessageHandler;
|
||||||
private final IotComponentRegistry componentRegistry;
|
private final IotNetComponentRegistry componentRegistry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务运行状态标志
|
* 服务运行状态标志
|
||||||
*/
|
*/
|
||||||
private volatile boolean isRunning = false;
|
private volatile boolean isRunning = false;
|
||||||
|
|
||||||
public IotDeviceUpstreamServer(IotComponentEmqxProperties emqxProperties,
|
public IotDeviceUpstreamServer(IotNetComponentEmqxProperties emqxProperties,
|
||||||
IotDeviceUpstreamApi deviceUpstreamApi,
|
IotDeviceUpstreamApi deviceUpstreamApi,
|
||||||
Vertx vertx,
|
Vertx vertx,
|
||||||
MqttClient client,
|
MqttClient client,
|
||||||
IotComponentRegistry componentRegistry) {
|
IotNetComponentRegistry componentRegistry) {
|
||||||
this.vertx = vertx;
|
this.vertx = vertx;
|
||||||
this.emqxProperties = emqxProperties;
|
this.emqxProperties = emqxProperties;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
@@ -70,8 +61,7 @@ public class IotDeviceUpstreamServer {
|
|||||||
Router router = Router.router(vertx);
|
Router router = Router.router(vertx);
|
||||||
router.route().handler(BodyHandler.create()); // 处理 Body
|
router.route().handler(BodyHandler.create()); // 处理 Body
|
||||||
router.post(IotDeviceAuthVertxHandler.PATH)
|
router.post(IotDeviceAuthVertxHandler.PATH)
|
||||||
// TODO @haohao:疑问,mqtt 的认证,需要通过 http 呀?
|
// MQTT 认证不必须通过 HTTP 进行,但 HTTP 认证是 EMQX 等 MQTT 服务器支持的一种灵活的认证方式
|
||||||
// 回复:MQTT 认证不必须通过 HTTP 进行,但 HTTP 认证是 EMQX 等 MQTT 服务器支持的一种灵活的认证方式
|
|
||||||
.handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi));
|
.handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi));
|
||||||
// 添加 Webhook 处理器,用于处理设备连接和断开连接事件
|
// 添加 Webhook 处理器,用于处理设备连接和断开连接事件
|
||||||
router.post(IotDeviceWebhookVertxHandler.PATH)
|
router.post(IotDeviceWebhookVertxHandler.PATH)
|
||||||
@@ -91,15 +81,20 @@ public class IotDeviceUpstreamServer {
|
|||||||
}
|
}
|
||||||
log.info("[start][开始启动服务]");
|
log.info("[start][开始启动服务]");
|
||||||
|
|
||||||
// 检查authPort是否为null
|
// 检查 authPort 是否为 null
|
||||||
Integer authPort = emqxProperties.getAuthPort();
|
Integer authPort = emqxProperties.getAuthPort();
|
||||||
if (authPort == null) {
|
if (authPort == null) {
|
||||||
log.warn("[start][authPort为null,使用默认端口8080]");
|
log.warn("[start][authPort 为 null,使用默认端口 8080]");
|
||||||
authPort = 8080; // 默认端口
|
authPort = 8080; // 默认端口
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取连接超时时间
|
||||||
|
int connectionTimeoutMs = emqxProperties.getConnectionTimeoutMs() != null
|
||||||
|
? emqxProperties.getConnectionTimeoutMs()
|
||||||
|
: 10000;
|
||||||
|
|
||||||
// 1. 启动 HTTP 服务器
|
// 1. 启动 HTTP 服务器
|
||||||
final Integer finalAuthPort = authPort; // 为lambda表达式创建final变量
|
final Integer finalAuthPort = authPort; // 为 lambda 表达式创建 final 变量
|
||||||
CompletableFuture<Void> httpFuture = server.listen(finalAuthPort)
|
CompletableFuture<Void> httpFuture = server.listen(finalAuthPort)
|
||||||
.toCompletionStage()
|
.toCompletionStage()
|
||||||
.toCompletableFuture()
|
.toCompletableFuture()
|
||||||
@@ -115,13 +110,13 @@ public class IotDeviceUpstreamServer {
|
|||||||
log.warn("[closeHandler][MQTT 连接已断开,准备重连]");
|
log.warn("[closeHandler][MQTT 连接已断开,准备重连]");
|
||||||
reconnectWithDelay();
|
reconnectWithDelay();
|
||||||
});
|
});
|
||||||
// 2. 设置 MQTT 消息处理器
|
// 2.2 设置 MQTT 消息处理器
|
||||||
setupMessageHandler();
|
setupMessageHandler();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. 等待所有服务启动完成
|
// 3. 等待所有服务启动完成
|
||||||
CompletableFuture.allOf(httpFuture, mqttFuture)
|
CompletableFuture.allOf(httpFuture, mqttFuture)
|
||||||
.orTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
.orTimeout(connectionTimeoutMs, TimeUnit.MILLISECONDS)
|
||||||
.whenComplete((result, error) -> {
|
.whenComplete((result, error) -> {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
log.error("[start][服务启动失败]", error);
|
log.error("[start][服务启动失败]", error);
|
||||||
@@ -149,7 +144,12 @@ public class IotDeviceUpstreamServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
vertx.setTimer(RECONNECT_DELAY_MS, id -> {
|
// 获取重连延迟时间
|
||||||
|
int reconnectDelayMs = emqxProperties.getReconnectDelayMs() != null
|
||||||
|
? emqxProperties.getReconnectDelayMs()
|
||||||
|
: 5000;
|
||||||
|
|
||||||
|
vertx.setTimer(reconnectDelayMs, id -> {
|
||||||
log.info("[reconnectWithDelay][开始重新连接 MQTT]");
|
log.info("[reconnectWithDelay][开始重新连接 MQTT]");
|
||||||
connectMqtt();
|
connectMqtt();
|
||||||
});
|
});
|
||||||
@@ -158,14 +158,14 @@ public class IotDeviceUpstreamServer {
|
|||||||
/**
|
/**
|
||||||
* 连接 MQTT Broker 并订阅主题
|
* 连接 MQTT Broker 并订阅主题
|
||||||
*
|
*
|
||||||
* @return 连接结果的Future
|
* @return 连接结果的 Future
|
||||||
*/
|
*/
|
||||||
private Future<Void> connectMqtt() {
|
private Future<Void> connectMqtt() {
|
||||||
// 检查必要的 MQTT 配置
|
// 检查必要的 MQTT 配置
|
||||||
String host = emqxProperties.getMqttHost();
|
String host = emqxProperties.getMqttHost();
|
||||||
Integer port = emqxProperties.getMqttPort();
|
Integer port = emqxProperties.getMqttPort();
|
||||||
if (host == null) {
|
if (StrUtil.isBlank(host)) {
|
||||||
String msg = "[connectMqtt][MQTT Host 为 null,无法连接]";
|
String msg = "[connectMqtt][MQTT Host 为空,无法连接]";
|
||||||
log.error(msg);
|
log.error(msg);
|
||||||
return Future.failedFuture(new IllegalStateException(msg));
|
return Future.failedFuture(new IllegalStateException(msg));
|
||||||
}
|
}
|
||||||
@@ -177,11 +177,11 @@ public class IotDeviceUpstreamServer {
|
|||||||
final Integer finalPort = port;
|
final Integer finalPort = port;
|
||||||
return client.connect(finalPort, host)
|
return client.connect(finalPort, host)
|
||||||
.compose(connAck -> {
|
.compose(connAck -> {
|
||||||
log.info("[connectMqtt][MQTT客户端连接成功]");
|
log.info("[connectMqtt][MQTT 客户端连接成功]");
|
||||||
return subscribeToTopics();
|
return subscribeToTopics();
|
||||||
})
|
})
|
||||||
.recover(error -> {
|
.recover(error -> {
|
||||||
log.error("[connectMqtt][连接MQTT Broker失败:]", error);
|
log.error("[connectMqtt][连接 MQTT Broker 失败:]", error);
|
||||||
reconnectWithDelay();
|
reconnectWithDelay();
|
||||||
return Future.failedFuture(error);
|
return Future.failedFuture(error);
|
||||||
});
|
});
|
||||||
@@ -198,62 +198,67 @@ public class IotDeviceUpstreamServer {
|
|||||||
log.warn("[subscribeToTopics][未配置 MQTT 主题或为 null,使用默认主题]");
|
log.warn("[subscribeToTopics][未配置 MQTT 主题或为 null,使用默认主题]");
|
||||||
topics = new String[]{"/device/#"}; // 默认订阅所有设备上下行主题
|
topics = new String[]{"/device/#"}; // 默认订阅所有设备上下行主题
|
||||||
}
|
}
|
||||||
log.info("[subscribeToTopics][开始订阅设备上行消息主题]");
|
|
||||||
|
|
||||||
Future<Void> compositeFuture = Future.succeededFuture();
|
// 使用协调器追踪多个 Future 的完成状态
|
||||||
|
Future<Void> result = Future.succeededFuture();
|
||||||
for (String topic : topics) {
|
for (String topic : topics) {
|
||||||
String trimmedTopic = StrUtil.trim(topic);
|
if (StrUtil.isBlank(topic)) {
|
||||||
if (StrUtil.isBlank(trimmedTopic)) {
|
log.warn("[subscribeToTopics][跳过空主题]");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
compositeFuture = compositeFuture.compose(v -> client.subscribe(trimmedTopic, DEFAULT_QOS.value())
|
|
||||||
|
result = result.compose(v -> client.subscribe(topic, DEFAULT_QOS.value())
|
||||||
.<Void>map(ack -> {
|
.<Void>map(ack -> {
|
||||||
log.info("[subscribeToTopics][成功订阅主题: {}]", trimmedTopic);
|
log.info("[subscribeToTopics][订阅主题成功: {}]", topic);
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.recover(error -> {
|
.recover(err -> {
|
||||||
log.error("[subscribeToTopics][订阅主题失败: {}]", trimmedTopic, error);
|
log.error("[subscribeToTopics][订阅主题失败: {}]", topic, err);
|
||||||
return Future.<Void>succeededFuture(); // 继续订阅其他主题
|
return Future.failedFuture(err);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return compositeFuture;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止所有服务
|
* 停止服务
|
||||||
*/
|
*/
|
||||||
public void stop() {
|
public void stop() {
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
log.warn("[stop][服务未运行,无需停止]");
|
log.warn("[stop][服务已经停止,无需再次停止]");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info("[stop][开始关闭服务]");
|
log.info("[stop][开始停止服务]");
|
||||||
isRunning = false;
|
|
||||||
|
|
||||||
try {
|
// 1. 取消 MQTT 主题订阅
|
||||||
CompletableFuture<Void> serverFuture = server != null
|
if (client.isConnected()) {
|
||||||
? server.close().toCompletionStage().toCompletableFuture()
|
for (String topic : emqxProperties.getMqttTopics()) {
|
||||||
: CompletableFuture.completedFuture(null);
|
try {
|
||||||
CompletableFuture<Void> clientFuture = client != null
|
client.unsubscribe(topic);
|
||||||
? client.disconnect().toCompletionStage().toCompletableFuture()
|
} catch (Exception e) {
|
||||||
: CompletableFuture.completedFuture(null);
|
log.warn("[stop][取消订阅主题异常: {}]", topic, e);
|
||||||
CompletableFuture<Void> vertxFuture = vertx != null
|
}
|
||||||
? vertx.close().toCompletionStage().toCompletableFuture()
|
}
|
||||||
: CompletableFuture.completedFuture(null);
|
|
||||||
|
|
||||||
// 等待所有资源关闭
|
|
||||||
CompletableFuture.allOf(serverFuture, clientFuture, vertxFuture)
|
|
||||||
.orTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
|
||||||
.whenComplete((result, error) -> {
|
|
||||||
if (error != null) {
|
|
||||||
log.error("[stop][服务关闭过程中发生异常]", error);
|
|
||||||
} else {
|
|
||||||
log.info("[stop][所有服务关闭完成]");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[stop][关闭服务异常]", e);
|
|
||||||
throw new RuntimeException("关闭 IoT 设备上行服务失败", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 关闭 MQTT 客户端
|
||||||
|
try {
|
||||||
|
if (client.isConnected()) {
|
||||||
|
client.disconnect();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[stop][关闭 MQTT 客户端异常]", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 关闭 HTTP 服务器
|
||||||
|
try {
|
||||||
|
server.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[stop][关闭 HTTP 服务器异常]", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 更新状态
|
||||||
|
isRunning = false;
|
||||||
|
log.info("[stop][服务已停止]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.emqx.upstream.router;
|
package cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEmqxAuthReqDTO;
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEmqxAuthReqDTO;
|
||||||
import cn.iocoder.yudao.module.iot.component.core.util.IotPluginCommonUtils;
|
import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils;
|
||||||
import io.vertx.core.Handler;
|
import io.vertx.core.Handler;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.ext.web.RoutingContext;
|
import io.vertx.ext.web.RoutingContext;
|
||||||
@@ -47,17 +47,17 @@ public class IotDeviceAuthVertxHandler implements Handler<RoutingContext> {
|
|||||||
CommonResult<Boolean> authResult = deviceUpstreamApi.authenticateEmqxConnection(authReqDTO);
|
CommonResult<Boolean> authResult = deviceUpstreamApi.authenticateEmqxConnection(authReqDTO);
|
||||||
if (authResult.getCode() != 0 || !authResult.getData()) {
|
if (authResult.getCode() != 0 || !authResult.getData()) {
|
||||||
// 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求
|
// 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求
|
||||||
IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny"));
|
IotNetComponentCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应结果
|
// 响应结果
|
||||||
// 注意:这里必须返回 {"result": "allow"} 格式,以符合 EMQX 认证插件的要求
|
// 注意:这里必须返回 {"result": "allow"} 格式,以符合 EMQX 认证插件的要求
|
||||||
IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "allow"));
|
IotNetComponentCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "allow"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[handle][EMQX 认证异常]", e);
|
log.error("[handle][EMQX 认证异常]", e);
|
||||||
// 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求
|
// 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求
|
||||||
IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny"));
|
IotNetComponentCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.emqx.upstream.router;
|
package cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONObject;
|
||||||
@@ -7,8 +7,9 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
|||||||
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
|
||||||
import cn.iocoder.yudao.module.iot.component.core.pojo.IotStandardResponse;
|
import cn.iocoder.yudao.module.iot.net.component.core.constants.IotDeviceTopicEnum;
|
||||||
import cn.iocoder.yudao.module.iot.component.core.util.IotPluginCommonUtils;
|
import cn.iocoder.yudao.module.iot.net.component.core.pojo.IotStandardResponse;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils;
|
||||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||||
import io.vertx.core.buffer.Buffer;
|
import io.vertx.core.buffer.Buffer;
|
||||||
import io.vertx.mqtt.MqttClient;
|
import io.vertx.mqtt.MqttClient;
|
||||||
@@ -23,25 +24,12 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* IoT 设备 MQTT 消息处理器
|
* IoT 设备 MQTT 消息处理器
|
||||||
* <p>
|
* <p>
|
||||||
* 参考:<a href="https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services">设备属性、事件、服务</a>
|
* 参考:<a href=
|
||||||
|
* "https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services">设备属性、事件、服务</a>
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class IotDeviceMqttMessageHandler {
|
public class IotDeviceMqttMessageHandler {
|
||||||
|
|
||||||
// TODO @haohao:讨论,感觉 mqtt 和 http,可以做个相对统一的格式哈;回复 都使用 Alink 格式,方便后续扩展。
|
|
||||||
// 设备上报属性 标准 JSON
|
|
||||||
// 请求 Topic:/sys/${productKey}/${deviceName}/thing/event/property/post
|
|
||||||
// 响应 Topic:/sys/${productKey}/${deviceName}/thing/event/property/post_reply
|
|
||||||
|
|
||||||
// 设备上报事件 标准 JSON
|
|
||||||
// 请求 Topic:/sys/${productKey}/${deviceName}/thing/event/${tsl.event.identifier}/post
|
|
||||||
// 响应 Topic:/sys/${productKey}/${deviceName}/thing/event/${tsl.event.identifier}/post_reply
|
|
||||||
|
|
||||||
private static final String SYS_TOPIC_PREFIX = "/sys/";
|
|
||||||
private static final String PROPERTY_POST_TOPIC = "/thing/event/property/post";
|
|
||||||
private static final String EVENT_POST_TOPIC_PREFIX = "/thing/event/";
|
|
||||||
private static final String EVENT_POST_TOPIC_SUFFIX = "/post";
|
|
||||||
private static final String REPLY_SUFFIX = "_reply";
|
|
||||||
private static final String PROPERTY_METHOD = "thing.event.property.post";
|
private static final String PROPERTY_METHOD = "thing.event.property.post";
|
||||||
private static final String EVENT_METHOD_PREFIX = "thing.event.";
|
private static final String EVENT_METHOD_PREFIX = "thing.event.";
|
||||||
private static final String EVENT_METHOD_SUFFIX = ".post";
|
private static final String EVENT_METHOD_SUFFIX = ".post";
|
||||||
@@ -83,20 +71,21 @@ public class IotDeviceMqttMessageHandler {
|
|||||||
*/
|
*/
|
||||||
private void handleMessage(String topic, String payload) {
|
private void handleMessage(String topic, String payload) {
|
||||||
// 校验前缀
|
// 校验前缀
|
||||||
if (!topic.startsWith(SYS_TOPIC_PREFIX)) {
|
if (!topic.startsWith(IotDeviceTopicEnum.SYS_TOPIC_PREFIX.getTopic())) {
|
||||||
log.warn("[handleMessage][未知的消息类型][topic: {}]", topic);
|
log.warn("[handleMessage][未知的消息类型][topic: {}]", topic);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理设备属性上报消息
|
// 处理设备属性上报消息
|
||||||
if (topic.endsWith(PROPERTY_POST_TOPIC)) {
|
if (topic.endsWith(IotDeviceTopicEnum.PROPERTY_POST_TOPIC.getTopic())) {
|
||||||
log.info("[handleMessage][接收到设备属性上报][topic: {}]", topic);
|
log.info("[handleMessage][接收到设备属性上报][topic: {}]", topic);
|
||||||
handlePropertyPost(topic, payload);
|
handlePropertyPost(topic, payload);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理设备事件上报消息
|
// 处理设备事件上报消息
|
||||||
if (topic.contains(EVENT_POST_TOPIC_PREFIX) && topic.endsWith(EVENT_POST_TOPIC_SUFFIX)) {
|
if (topic.contains(IotDeviceTopicEnum.EVENT_POST_TOPIC_PREFIX.getTopic()) &&
|
||||||
|
topic.endsWith(IotDeviceTopicEnum.EVENT_POST_TOPIC_SUFFIX.getTopic())) {
|
||||||
log.info("[handleMessage][接收到设备事件上报][topic: {}]", topic);
|
log.info("[handleMessage][接收到设备事件上报][topic: {}]", topic);
|
||||||
handleEventPost(topic, payload);
|
handleEventPost(topic, payload);
|
||||||
return;
|
return;
|
||||||
@@ -212,7 +201,7 @@ public class IotDeviceMqttMessageHandler {
|
|||||||
* @param customData 自定义数据,可为 null
|
* @param customData 自定义数据,可为 null
|
||||||
*/
|
*/
|
||||||
private void sendResponse(String topic, JSONObject jsonObject, String method, Object customData) {
|
private void sendResponse(String topic, JSONObject jsonObject, String method, Object customData) {
|
||||||
String replyTopic = topic + REPLY_SUFFIX;
|
String replyTopic = IotDeviceTopicEnum.getReplyTopic(topic);
|
||||||
|
|
||||||
// 响应结果
|
// 响应结果
|
||||||
IotStandardResponse response = IotStandardResponse.success(
|
IotStandardResponse response = IotStandardResponse.success(
|
||||||
@@ -236,7 +225,7 @@ public class IotDeviceMqttMessageHandler {
|
|||||||
private IotDevicePropertyReportReqDTO buildPropertyReportDTO(JSONObject jsonObject, String[] topicParts) {
|
private IotDevicePropertyReportReqDTO buildPropertyReportDTO(JSONObject jsonObject, String[] topicParts) {
|
||||||
IotDevicePropertyReportReqDTO reportReqDTO = new IotDevicePropertyReportReqDTO();
|
IotDevicePropertyReportReqDTO reportReqDTO = new IotDevicePropertyReportReqDTO();
|
||||||
reportReqDTO.setRequestId(jsonObject.getStr("id"));
|
reportReqDTO.setRequestId(jsonObject.getStr("id"));
|
||||||
reportReqDTO.setProcessId(IotPluginCommonUtils.getProcessId());
|
reportReqDTO.setProcessId(IotNetComponentCommonUtils.getProcessId());
|
||||||
reportReqDTO.setReportTime(LocalDateTime.now());
|
reportReqDTO.setReportTime(LocalDateTime.now());
|
||||||
reportReqDTO.setProductKey(topicParts[2]);
|
reportReqDTO.setProductKey(topicParts[2]);
|
||||||
reportReqDTO.setDeviceName(topicParts[3]);
|
reportReqDTO.setDeviceName(topicParts[3]);
|
||||||
@@ -276,7 +265,7 @@ public class IotDeviceMqttMessageHandler {
|
|||||||
private IotDeviceEventReportReqDTO buildEventReportDTO(JSONObject jsonObject, String[] topicParts) {
|
private IotDeviceEventReportReqDTO buildEventReportDTO(JSONObject jsonObject, String[] topicParts) {
|
||||||
IotDeviceEventReportReqDTO reportReqDTO = new IotDeviceEventReportReqDTO();
|
IotDeviceEventReportReqDTO reportReqDTO = new IotDeviceEventReportReqDTO();
|
||||||
reportReqDTO.setRequestId(jsonObject.getStr("id"));
|
reportReqDTO.setRequestId(jsonObject.getStr("id"));
|
||||||
reportReqDTO.setProcessId(IotPluginCommonUtils.getProcessId());
|
reportReqDTO.setProcessId(IotNetComponentCommonUtils.getProcessId());
|
||||||
reportReqDTO.setReportTime(LocalDateTime.now());
|
reportReqDTO.setReportTime(LocalDateTime.now());
|
||||||
reportReqDTO.setProductKey(topicParts[2]);
|
reportReqDTO.setProductKey(topicParts[2]);
|
||||||
reportReqDTO.setDeviceName(topicParts[3]);
|
reportReqDTO.setDeviceName(topicParts[3]);
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.emqx.upstream.router;
|
package cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
|
||||||
import cn.iocoder.yudao.module.iot.component.core.util.IotPluginCommonUtils;
|
|
||||||
import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum;
|
import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils;
|
||||||
import io.vertx.core.Handler;
|
import io.vertx.core.Handler;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.ext.web.RoutingContext;
|
import io.vertx.ext.web.RoutingContext;
|
||||||
@@ -57,11 +57,11 @@ public class IotDeviceWebhookVertxHandler implements Handler<RoutingContext> {
|
|||||||
|
|
||||||
// 返回成功响应
|
// 返回成功响应
|
||||||
// 注意:这里必须返回 {"result": "success"} 格式,以符合 EMQX Webhook 插件的要求
|
// 注意:这里必须返回 {"result": "success"} 格式,以符合 EMQX Webhook 插件的要求
|
||||||
IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "success"));
|
IotNetComponentCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "success"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[handle][处理 Webhook 事件异常]", e);
|
log.error("[handle][处理 Webhook 事件异常]", e);
|
||||||
// 注意:这里必须返回 {"result": "error"} 格式,以符合 EMQX Webhook 插件的要求
|
// 注意:这里必须返回 {"result": "error"} 格式,以符合 EMQX Webhook 插件的要求
|
||||||
IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "error"));
|
IotNetComponentCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "error"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ public class IotDeviceWebhookVertxHandler implements Handler<RoutingContext> {
|
|||||||
updateReqDTO.setProductKey(parts[1]);
|
updateReqDTO.setProductKey(parts[1]);
|
||||||
updateReqDTO.setDeviceName(parts[0]);
|
updateReqDTO.setDeviceName(parts[0]);
|
||||||
updateReqDTO.setState(IotDeviceStateEnum.ONLINE.getState());
|
updateReqDTO.setState(IotDeviceStateEnum.ONLINE.getState());
|
||||||
updateReqDTO.setProcessId(IotPluginCommonUtils.getProcessId());
|
updateReqDTO.setProcessId(IotNetComponentCommonUtils.getProcessId());
|
||||||
updateReqDTO.setReportTime(LocalDateTime.now());
|
updateReqDTO.setReportTime(LocalDateTime.now());
|
||||||
CommonResult<Boolean> result = deviceUpstreamApi.updateDeviceState(updateReqDTO);
|
CommonResult<Boolean> result = deviceUpstreamApi.updateDeviceState(updateReqDTO);
|
||||||
if (result.getCode() != 0 || !result.getData()) {
|
if (result.getCode() != 0 || !result.getData()) {
|
||||||
@@ -120,7 +120,7 @@ public class IotDeviceWebhookVertxHandler implements Handler<RoutingContext> {
|
|||||||
offlineReqDTO.setProductKey(parts[1]);
|
offlineReqDTO.setProductKey(parts[1]);
|
||||||
offlineReqDTO.setDeviceName(parts[0]);
|
offlineReqDTO.setDeviceName(parts[0]);
|
||||||
offlineReqDTO.setState(IotDeviceStateEnum.OFFLINE.getState());
|
offlineReqDTO.setState(IotDeviceStateEnum.OFFLINE.getState());
|
||||||
offlineReqDTO.setProcessId(IotPluginCommonUtils.getProcessId());
|
offlineReqDTO.setProcessId(IotNetComponentCommonUtils.getProcessId());
|
||||||
offlineReqDTO.setReportTime(LocalDateTime.now());
|
offlineReqDTO.setReportTime(LocalDateTime.now());
|
||||||
CommonResult<Boolean> offlineResult = deviceUpstreamApi.updateDeviceState(offlineReqDTO);
|
CommonResult<Boolean> offlineResult = deviceUpstreamApi.updateDeviceState(offlineReqDTO);
|
||||||
if (offlineResult.getCode() != 0 || !offlineResult.getData()) {
|
if (offlineResult.getCode() != 0 || !offlineResult.getData()) {
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
cn.iocoder.yudao.module.iot.net.component.emqx.config.IotNetComponentEmqxAutoConfiguration
|
||||||
@@ -3,23 +3,23 @@
|
|||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<parent>
|
<parent>
|
||||||
<artifactId>yudao-module-iot-components</artifactId>
|
<artifactId>yudao-module-iot-net-components</artifactId>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<artifactId>yudao-module-iot-component-http</artifactId>
|
<artifactId>yudao-module-iot-net-component-http</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>${project.artifactId}</name>
|
<name>${project.artifactId}</name>
|
||||||
<description>
|
<description>
|
||||||
物联网组件 HTTP 模块
|
物联网网络组件 HTTP 模块
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-module-iot-component-core</artifactId>
|
<artifactId>yudao-module-iot-net-component-core</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.net.component.http.config;
|
||||||
|
|
||||||
|
import cn.hutool.system.SystemUtil;
|
||||||
|
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.http.downstream.IotDeviceDownstreamHandlerImpl;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.http.upstream.IotDeviceUpstreamServer;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.config.IotNetComponentCommonProperties;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.downstream.IotDeviceDownstreamHandler;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.heartbeat.IotNetComponentRegistry;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils;
|
||||||
|
import io.vertx.core.Vertx;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT 网络组件 HTTP 的自动配置类
|
||||||
|
*
|
||||||
|
* @author haohao
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@AutoConfiguration
|
||||||
|
@EnableConfigurationProperties(IotNetComponentHttpProperties.class)
|
||||||
|
@ConditionalOnProperty(prefix = "yudao.iot.component.http", name = "enabled", havingValue = "true", matchIfMissing = false)
|
||||||
|
@ComponentScan(basePackages = {
|
||||||
|
"cn.iocoder.yudao.module.iot.net.component.http" // 只扫描 HTTP 组件包
|
||||||
|
})
|
||||||
|
public class IotNetComponentHttpAutoConfiguration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件 key
|
||||||
|
*/
|
||||||
|
private static final String PLUGIN_KEY = "http";
|
||||||
|
|
||||||
|
public IotNetComponentHttpAutoConfiguration() {
|
||||||
|
// 构造函数中不输出日志,移到 initialize 方法中
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 HTTP 组件
|
||||||
|
*
|
||||||
|
* @param event 应用启动事件
|
||||||
|
*/
|
||||||
|
@EventListener(ApplicationStartedEvent.class)
|
||||||
|
public void initialize(ApplicationStartedEvent event) {
|
||||||
|
log.info("[IotNetComponentHttpAutoConfiguration][开始初始化]");
|
||||||
|
|
||||||
|
// 从应用上下文中获取需要的 Bean
|
||||||
|
IotNetComponentRegistry componentRegistry = event.getApplicationContext()
|
||||||
|
.getBean(IotNetComponentRegistry.class);
|
||||||
|
IotNetComponentCommonProperties commonProperties = event.getApplicationContext()
|
||||||
|
.getBean(IotNetComponentCommonProperties.class);
|
||||||
|
|
||||||
|
// 设置当前组件的核心标识
|
||||||
|
// 注意:这里只为当前 HTTP 组件设置 pluginKey,不影响其他组件
|
||||||
|
commonProperties.setPluginKey(PLUGIN_KEY);
|
||||||
|
|
||||||
|
// 将 HTTP 组件注册到组件注册表
|
||||||
|
componentRegistry.registerComponent(
|
||||||
|
PLUGIN_KEY,
|
||||||
|
SystemUtil.getHostInfo().getAddress(),
|
||||||
|
0, // 内嵌模式固定为 0
|
||||||
|
IotNetComponentCommonUtils.getProcessId());
|
||||||
|
|
||||||
|
log.info("[initialize][IoT HTTP 组件初始化完成]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Vert.x 实例
|
||||||
|
*
|
||||||
|
* @return Vert.x 实例
|
||||||
|
*/
|
||||||
|
@Bean(name = "httpVertx")
|
||||||
|
public Vertx vertx() {
|
||||||
|
return Vertx.vertx();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建设备上行服务器
|
||||||
|
*
|
||||||
|
* @param vertx Vert.x 实例
|
||||||
|
* @param deviceUpstreamApi 设备上行 API
|
||||||
|
* @param properties HTTP 组件配置属性
|
||||||
|
* @param applicationContext 应用上下文
|
||||||
|
* @return 设备上行服务器
|
||||||
|
*/
|
||||||
|
@Bean(name = "httpDeviceUpstreamServer", initMethod = "start", destroyMethod = "stop")
|
||||||
|
public IotDeviceUpstreamServer deviceUpstreamServer(
|
||||||
|
@Lazy @Qualifier("httpVertx") Vertx vertx,
|
||||||
|
IotDeviceUpstreamApi deviceUpstreamApi,
|
||||||
|
IotNetComponentHttpProperties properties,
|
||||||
|
ApplicationContext applicationContext) {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("HTTP 服务器配置: port={}", properties.getServerPort());
|
||||||
|
} else {
|
||||||
|
log.info("HTTP 服务器将监听端口: {}", properties.getServerPort());
|
||||||
|
}
|
||||||
|
return new IotDeviceUpstreamServer(vertx, properties, deviceUpstreamApi, applicationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建设备下行处理器
|
||||||
|
*
|
||||||
|
* @return 设备下行处理器
|
||||||
|
*/
|
||||||
|
@Bean(name = "httpDeviceDownstreamHandler")
|
||||||
|
public IotDeviceDownstreamHandler deviceDownstreamHandler() {
|
||||||
|
return new IotDeviceDownstreamHandlerImpl();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.http.config;
|
package cn.iocoder.yudao.module.iot.net.component.http.config;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IoT HTTP 组件配置属性
|
* IoT HTTP 网络组件配置属性
|
||||||
|
*
|
||||||
|
* @author haohao
|
||||||
*/
|
*/
|
||||||
@ConfigurationProperties(prefix = "yudao.iot.component.http")
|
@ConfigurationProperties(prefix = "yudao.iot.component.http")
|
||||||
@Validated
|
@Validated
|
||||||
@Data
|
@Data
|
||||||
public class IotComponentHttpProperties {
|
public class IotNetComponentHttpProperties {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否启用
|
* 是否启用 HTTP 组件
|
||||||
*/
|
*/
|
||||||
private Boolean enabled;
|
private Boolean enabled;
|
||||||
|
|
||||||
@@ -22,4 +24,10 @@ public class IotComponentHttpProperties {
|
|||||||
*/
|
*/
|
||||||
private Integer serverPort;
|
private Integer serverPort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接超时时间(毫秒)
|
||||||
|
* <p>
|
||||||
|
* 默认值:10000 毫秒
|
||||||
|
*/
|
||||||
|
private Integer connectionTimeoutMs = 10000;
|
||||||
}
|
}
|
||||||
@@ -1,44 +1,50 @@
|
|||||||
package cn.iocoder.yudao.module.iot.component.http.downstream;
|
package cn.iocoder.yudao.module.iot.net.component.http.downstream;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
|
||||||
import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamHandler;
|
import cn.iocoder.yudao.module.iot.net.component.core.downstream.IotDeviceDownstreamHandler;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED;
|
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP 插件的 {@link IotDeviceDownstreamHandler} 实现类
|
* HTTP 网络组件的 {@link IotDeviceDownstreamHandler} 实现类
|
||||||
* <p>
|
* <p>
|
||||||
* 但是:由于设备通过 HTTP 短链接接入,导致其实无法下行指导给 device 设备,所以基本都是直接返回失败!!!
|
* 但是:由于设备通过 HTTP 短链接接入,导致其实无法下行指导给 device 设备,所以基本都是直接返回失败!!!
|
||||||
* 类似 MQTT、WebSocket、TCP 插件,是可以实现下行指令的。
|
* 类似 MQTT、WebSocket、TCP 网络组件,是可以实现下行指令的。
|
||||||
*
|
*
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler {
|
public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不支持的错误消息
|
||||||
|
*/
|
||||||
|
private static final String NOT_SUPPORTED_MSG = "HTTP 不支持设备下行通信";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResult<Boolean> invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO) {
|
public CommonResult<Boolean> invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO) {
|
||||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持调用设备服务");
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(), NOT_SUPPORTED_MSG);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResult<Boolean> getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) {
|
public CommonResult<Boolean> getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) {
|
||||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持获取设备属性");
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(), NOT_SUPPORTED_MSG);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResult<Boolean> setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO) {
|
public CommonResult<Boolean> setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO) {
|
||||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性");
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(), NOT_SUPPORTED_MSG);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResult<Boolean> setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) {
|
public CommonResult<Boolean> setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) {
|
||||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性");
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(), NOT_SUPPORTED_MSG);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResult<Boolean> upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) {
|
public CommonResult<Boolean> upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) {
|
||||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性");
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(), NOT_SUPPORTED_MSG);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.net.component.http.upstream;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.http.config.IotNetComponentHttpProperties;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.http.upstream.router.IotDeviceUpstreamVertxHandler;
|
||||||
|
import io.vertx.core.AbstractVerticle;
|
||||||
|
import io.vertx.core.Promise;
|
||||||
|
import io.vertx.core.Vertx;
|
||||||
|
import io.vertx.ext.web.Router;
|
||||||
|
import io.vertx.ext.web.handler.BodyHandler;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT 设备上行服务器
|
||||||
|
* <p>
|
||||||
|
* 处理设备通过 HTTP 方式接入的上行消息
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class IotDeviceUpstreamServer extends AbstractVerticle {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vert.x 实例
|
||||||
|
*/
|
||||||
|
private final Vertx vertx;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP 组件配置属性
|
||||||
|
*/
|
||||||
|
private final IotNetComponentHttpProperties httpProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备上行 API
|
||||||
|
*/
|
||||||
|
private final IotDeviceUpstreamApi deviceUpstreamApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring 应用上下文
|
||||||
|
*/
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param vertx Vert.x 实例
|
||||||
|
* @param httpProperties HTTP 组件配置属性
|
||||||
|
* @param deviceUpstreamApi 设备上行 API
|
||||||
|
* @param applicationContext Spring 应用上下文
|
||||||
|
*/
|
||||||
|
public IotDeviceUpstreamServer(
|
||||||
|
@Lazy Vertx vertx,
|
||||||
|
IotNetComponentHttpProperties httpProperties,
|
||||||
|
IotDeviceUpstreamApi deviceUpstreamApi,
|
||||||
|
ApplicationContext applicationContext) {
|
||||||
|
this.vertx = vertx;
|
||||||
|
this.httpProperties = httpProperties;
|
||||||
|
this.deviceUpstreamApi = deviceUpstreamApi;
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(Promise<Void> startPromise) {
|
||||||
|
// 创建路由
|
||||||
|
Router router = Router.router(vertx);
|
||||||
|
router.route().handler(BodyHandler.create());
|
||||||
|
|
||||||
|
// 创建处理器
|
||||||
|
IotDeviceUpstreamVertxHandler upstreamHandler = new IotDeviceUpstreamVertxHandler(
|
||||||
|
deviceUpstreamApi, applicationContext);
|
||||||
|
|
||||||
|
// 添加路由处理器
|
||||||
|
router.post(IotDeviceUpstreamVertxHandler.PROPERTY_PATH).handler(upstreamHandler::handle);
|
||||||
|
router.post(IotDeviceUpstreamVertxHandler.EVENT_PATH).handler(upstreamHandler::handle);
|
||||||
|
|
||||||
|
// 启动 HTTP 服务器
|
||||||
|
vertx.createHttpServer()
|
||||||
|
.requestHandler(router)
|
||||||
|
.listen(httpProperties.getServerPort(), result -> {
|
||||||
|
if (result.succeeded()) {
|
||||||
|
log.info("[start][IoT 设备上行服务器启动成功,端口:{}]", httpProperties.getServerPort());
|
||||||
|
startPromise.complete();
|
||||||
|
} else {
|
||||||
|
log.error("[start][IoT 设备上行服务器启动失败]", result.cause());
|
||||||
|
startPromise.fail(result.cause());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop(Promise<Void> stopPromise) {
|
||||||
|
log.info("[stop][IoT 设备上行服务器已停止]");
|
||||||
|
stopPromise.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.net.component.http.upstream.auth;
|
||||||
|
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.ext.web.RoutingContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT 设备认证提供者
|
||||||
|
* <p>
|
||||||
|
* 用于 HTTP 设备接入时的身份认证
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class IotDeviceAuthProvider {
|
||||||
|
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param applicationContext Spring 应用上下文
|
||||||
|
*/
|
||||||
|
public IotDeviceAuthProvider(ApplicationContext applicationContext) {
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证设备
|
||||||
|
*
|
||||||
|
* @param context 路由上下文
|
||||||
|
* @param clientId 设备唯一标识
|
||||||
|
* @return 认证结果 Future 对象
|
||||||
|
*/
|
||||||
|
public Future<Void> authenticate(RoutingContext context, String clientId) {
|
||||||
|
if (clientId == null || clientId.isEmpty()) {
|
||||||
|
return Future.failedFuture("clientId 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("[authenticate][设备认证成功,clientId={}]", clientId);
|
||||||
|
return Future.succeededFuture();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[authenticate][设备认证异常,clientId={}]", clientId, e);
|
||||||
|
return Future.failedFuture(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.net.component.http.upstream.router;
|
||||||
|
|
||||||
|
import cn.hutool.core.map.MapUtil;
|
||||||
|
import cn.hutool.core.util.IdUtil;
|
||||||
|
import cn.hutool.core.util.ObjUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
|
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
|
||||||
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
|
||||||
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
|
||||||
|
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
|
||||||
|
import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.constants.IotDeviceTopicEnum;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.pojo.IotStandardResponse;
|
||||||
|
import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils;
|
||||||
|
import io.vertx.core.Handler;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import io.vertx.ext.web.RoutingContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
|
||||||
|
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT 设备上行统一处理的 Vert.x Handler
|
||||||
|
* <p>
|
||||||
|
* 统一处理设备属性上报和事件上报的请求。
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class IotDeviceUpstreamVertxHandler implements Handler<RoutingContext> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 属性上报路径
|
||||||
|
*/
|
||||||
|
public static final String PROPERTY_PATH = "/sys/:productKey/:deviceName"
|
||||||
|
+ IotDeviceTopicEnum.PROPERTY_POST_TOPIC.getTopic();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件上报路径
|
||||||
|
*/
|
||||||
|
public static final String EVENT_PATH = "/sys/:productKey/:deviceName"
|
||||||
|
+ IotDeviceTopicEnum.EVENT_POST_TOPIC_PREFIX.getTopic() + ":identifier"
|
||||||
|
+ IotDeviceTopicEnum.EVENT_POST_TOPIC_SUFFIX.getTopic();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 属性上报方法标识
|
||||||
|
*/
|
||||||
|
private static final String PROPERTY_METHOD = "thing.event.property.post";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件上报方法前缀
|
||||||
|
*/
|
||||||
|
private static final String EVENT_METHOD_PREFIX = "thing.event.";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件上报方法后缀
|
||||||
|
*/
|
||||||
|
private static final String EVENT_METHOD_SUFFIX = ".post";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备上行 API
|
||||||
|
*/
|
||||||
|
private final IotDeviceUpstreamApi deviceUpstreamApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param deviceUpstreamApi 设备上行 API
|
||||||
|
* @param applicationContext 应用上下文
|
||||||
|
*/
|
||||||
|
public IotDeviceUpstreamVertxHandler(IotDeviceUpstreamApi deviceUpstreamApi,
|
||||||
|
ApplicationContext applicationContext) {
|
||||||
|
this.deviceUpstreamApi = deviceUpstreamApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(RoutingContext routingContext) {
|
||||||
|
String path = routingContext.request().path();
|
||||||
|
String requestId = IdUtil.fastSimpleUUID();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 解析通用参数
|
||||||
|
Map<String, String> params = parseCommonParams(routingContext, requestId);
|
||||||
|
String productKey = params.get("productKey");
|
||||||
|
String deviceName = params.get("deviceName");
|
||||||
|
JsonObject body = routingContext.body().asJsonObject();
|
||||||
|
requestId = params.get("requestId");
|
||||||
|
|
||||||
|
// 2. 根据路径模式处理不同类型的请求
|
||||||
|
if (isPropertyPostPath(path)) {
|
||||||
|
// 处理属性上报
|
||||||
|
handlePropertyPost(routingContext, productKey, deviceName, requestId, body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEventPostPath(path)) {
|
||||||
|
// 处理事件上报
|
||||||
|
String identifier = routingContext.pathParam("identifier");
|
||||||
|
handleEventPost(routingContext, productKey, deviceName, identifier, requestId, body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不支持的请求路径
|
||||||
|
sendErrorResponse(routingContext, requestId, "unknown", BAD_REQUEST.getCode(), "不支持的请求路径");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[handle][处理上行请求异常] path={}", path, e);
|
||||||
|
String method = determineMethodFromPath(path, routingContext);
|
||||||
|
sendErrorResponse(routingContext, requestId, method, INTERNAL_SERVER_ERROR.getCode(),
|
||||||
|
INTERNAL_SERVER_ERROR.getMsg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析通用参数
|
||||||
|
*
|
||||||
|
* @param routingContext 路由上下文
|
||||||
|
* @param defaultRequestId 默认请求 ID
|
||||||
|
* @return 参数映射
|
||||||
|
*/
|
||||||
|
private Map<String, String> parseCommonParams(RoutingContext routingContext, String defaultRequestId) {
|
||||||
|
Map<String, String> params = MapUtil.newHashMap();
|
||||||
|
params.put("productKey", routingContext.pathParam("productKey"));
|
||||||
|
params.put("deviceName", routingContext.pathParam("deviceName"));
|
||||||
|
|
||||||
|
JsonObject body = routingContext.body().asJsonObject();
|
||||||
|
String requestId = ObjUtil.defaultIfNull(body.getString("id"), defaultRequestId);
|
||||||
|
params.put("requestId", requestId);
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否是属性上报路径
|
||||||
|
*
|
||||||
|
* @param path 路径
|
||||||
|
* @return 是否是属性上报路径
|
||||||
|
*/
|
||||||
|
private boolean isPropertyPostPath(String path) {
|
||||||
|
return StrUtil.endWith(path, IotDeviceTopicEnum.PROPERTY_POST_TOPIC.getTopic());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否是事件上报路径
|
||||||
|
*
|
||||||
|
* @param path 路径
|
||||||
|
* @return 是否是事件上报路径
|
||||||
|
*/
|
||||||
|
private boolean isEventPostPath(String path) {
|
||||||
|
return StrUtil.contains(path, IotDeviceTopicEnum.EVENT_POST_TOPIC_PREFIX.getTopic())
|
||||||
|
&& StrUtil.endWith(path, IotDeviceTopicEnum.EVENT_POST_TOPIC_SUFFIX.getTopic());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理属性上报请求
|
||||||
|
*
|
||||||
|
* @param routingContext 路由上下文
|
||||||
|
* @param productKey 产品 Key
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param body 请求体
|
||||||
|
*/
|
||||||
|
private void handlePropertyPost(RoutingContext routingContext, String productKey, String deviceName,
|
||||||
|
String requestId, JsonObject body) {
|
||||||
|
// 处理属性上报
|
||||||
|
IotDevicePropertyReportReqDTO reportReqDTO = parsePropertyReportRequest(productKey, deviceName,
|
||||||
|
requestId, body);
|
||||||
|
|
||||||
|
// 设备上线
|
||||||
|
updateDeviceState(reportReqDTO.getProductKey(), reportReqDTO.getDeviceName());
|
||||||
|
|
||||||
|
// 属性上报
|
||||||
|
CommonResult<Boolean> result = deviceUpstreamApi.reportDeviceProperty(reportReqDTO);
|
||||||
|
|
||||||
|
// 返回响应
|
||||||
|
sendResponse(routingContext, requestId, PROPERTY_METHOD, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理事件上报请求
|
||||||
|
*
|
||||||
|
* @param routingContext 路由上下文
|
||||||
|
* @param productKey 产品 Key
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
* @param identifier 事件标识符
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param body 请求体
|
||||||
|
*/
|
||||||
|
private void handleEventPost(RoutingContext routingContext, String productKey, String deviceName,
|
||||||
|
String identifier, String requestId, JsonObject body) {
|
||||||
|
// 处理事件上报
|
||||||
|
IotDeviceEventReportReqDTO reportReqDTO = parseEventReportRequest(productKey, deviceName, identifier,
|
||||||
|
requestId, body);
|
||||||
|
|
||||||
|
// 设备上线
|
||||||
|
updateDeviceState(reportReqDTO.getProductKey(), reportReqDTO.getDeviceName());
|
||||||
|
|
||||||
|
// 事件上报
|
||||||
|
CommonResult<Boolean> result = deviceUpstreamApi.reportDeviceEvent(reportReqDTO);
|
||||||
|
String method = EVENT_METHOD_PREFIX + identifier + EVENT_METHOD_SUFFIX;
|
||||||
|
|
||||||
|
// 返回响应
|
||||||
|
sendResponse(routingContext, requestId, method, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送响应
|
||||||
|
*
|
||||||
|
* @param routingContext 路由上下文
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param method 方法名
|
||||||
|
* @param result 结果
|
||||||
|
*/
|
||||||
|
private void sendResponse(RoutingContext routingContext, String requestId, String method,
|
||||||
|
CommonResult<Boolean> result) {
|
||||||
|
IotStandardResponse response;
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
response = IotStandardResponse.success(requestId, method, result.getData());
|
||||||
|
} else {
|
||||||
|
response = IotStandardResponse.error(requestId, method, result.getCode(), result.getMsg());
|
||||||
|
}
|
||||||
|
IotNetComponentCommonUtils.writeJsonResponse(routingContext, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送错误响应
|
||||||
|
*
|
||||||
|
* @param routingContext 路由上下文
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param method 方法名
|
||||||
|
* @param code 错误代码
|
||||||
|
* @param message 错误消息
|
||||||
|
*/
|
||||||
|
private void sendErrorResponse(RoutingContext routingContext, String requestId, String method, Integer code,
|
||||||
|
String message) {
|
||||||
|
IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, code, message);
|
||||||
|
IotNetComponentCommonUtils.writeJsonResponse(routingContext, errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从路径确定方法名
|
||||||
|
*
|
||||||
|
* @param path 路径
|
||||||
|
* @param routingContext 路由上下文
|
||||||
|
* @return 方法名
|
||||||
|
*/
|
||||||
|
private String determineMethodFromPath(String path, RoutingContext routingContext) {
|
||||||
|
if (StrUtil.contains(path, "/property/")) {
|
||||||
|
return PROPERTY_METHOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EVENT_METHOD_PREFIX +
|
||||||
|
(routingContext.pathParams().containsKey("identifier")
|
||||||
|
? routingContext.pathParam("identifier")
|
||||||
|
: "unknown")
|
||||||
|
+
|
||||||
|
EVENT_METHOD_SUFFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新设备状态
|
||||||
|
*
|
||||||
|
* @param productKey 产品 Key
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
*/
|
||||||
|
private void updateDeviceState(String productKey, String deviceName) {
|
||||||
|
IotDeviceStateUpdateReqDTO reqDTO = ((IotDeviceStateUpdateReqDTO) new IotDeviceStateUpdateReqDTO()
|
||||||
|
.setRequestId(IdUtil.fastSimpleUUID())
|
||||||
|
.setProcessId(IotNetComponentCommonUtils.getProcessId())
|
||||||
|
.setReportTime(LocalDateTime.now())
|
||||||
|
.setProductKey(productKey)
|
||||||
|
.setDeviceName(deviceName)).setState(IotDeviceStateEnum.ONLINE.getState());
|
||||||
|
|
||||||
|
deviceUpstreamApi.updateDeviceState(reqDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析属性上报请求
|
||||||
|
*
|
||||||
|
* @param productKey 产品 Key
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param body 请求体
|
||||||
|
* @return 属性上报请求 DTO
|
||||||
|
*/
|
||||||
|
private IotDevicePropertyReportReqDTO parsePropertyReportRequest(String productKey, String deviceName,
|
||||||
|
String requestId, JsonObject body) {
|
||||||
|
// 解析属性
|
||||||
|
Map<String, Object> properties = parsePropertiesFromBody(body);
|
||||||
|
|
||||||
|
// 构建属性上报请求 DTO
|
||||||
|
return ((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO()
|
||||||
|
.setRequestId(requestId)
|
||||||
|
.setProcessId(IotNetComponentCommonUtils.getProcessId())
|
||||||
|
.setReportTime(LocalDateTime.now())
|
||||||
|
.setProductKey(productKey)
|
||||||
|
.setDeviceName(deviceName)).setProperties(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从请求体解析属性
|
||||||
|
*
|
||||||
|
* @param body 请求体
|
||||||
|
* @return 属性映射
|
||||||
|
*/
|
||||||
|
private Map<String, Object> parsePropertiesFromBody(JsonObject body) {
|
||||||
|
Map<String, Object> properties = MapUtil.newHashMap();
|
||||||
|
JsonObject params = body.getJsonObject("params");
|
||||||
|
|
||||||
|
if (params == null) {
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将标准格式的 params 转换为平台需要的 properties 格式
|
||||||
|
for (String key : params.fieldNames()) {
|
||||||
|
Object valueObj = params.getValue(key);
|
||||||
|
// 如果是复杂结构(包含 value 和 time)
|
||||||
|
if (valueObj instanceof JsonObject) {
|
||||||
|
JsonObject valueJson = (JsonObject) valueObj;
|
||||||
|
properties.put(key, valueJson.containsKey("value") ? valueJson.getValue("value") : valueObj);
|
||||||
|
} else {
|
||||||
|
properties.put(key, valueObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析事件上报请求
|
||||||
|
*
|
||||||
|
* @param productKey 产品 Key
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
* @param identifier 事件标识符
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param body 请求体
|
||||||
|
* @return 事件上报请求 DTO
|
||||||
|
*/
|
||||||
|
private IotDeviceEventReportReqDTO parseEventReportRequest(String productKey, String deviceName, String identifier,
|
||||||
|
String requestId, JsonObject body) {
|
||||||
|
// 解析参数
|
||||||
|
Map<String, Object> params = parseParamsFromBody(body);
|
||||||
|
|
||||||
|
// 构建事件上报请求 DTO
|
||||||
|
return ((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO()
|
||||||
|
.setRequestId(requestId)
|
||||||
|
.setProcessId(IotNetComponentCommonUtils.getProcessId())
|
||||||
|
.setReportTime(LocalDateTime.now())
|
||||||
|
.setProductKey(productKey)
|
||||||
|
.setDeviceName(deviceName)).setIdentifier(identifier).setParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从请求体解析参数
|
||||||
|
*
|
||||||
|
* @param body 请求体
|
||||||
|
* @return 参数映射
|
||||||
|
*/
|
||||||
|
private Map<String, Object> parseParamsFromBody(JsonObject body) {
|
||||||
|
Map<String, Object> params = MapUtil.newHashMap();
|
||||||
|
JsonObject paramsJson = body.getJsonObject("params");
|
||||||
|
|
||||||
|
if (paramsJson == null) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String key : paramsJson.fieldNames()) {
|
||||||
|
params.put(key, paramsJson.getValue(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
cn.iocoder.yudao.module.iot.net.component.http.config.IotNetComponentHttpAutoConfiguration
|
||||||
Reference in New Issue
Block a user