Commit 495ae4526b806c8f42bf012f6e273c5ee8b19e45

Authored by alwayssuper
2 parents 62a868f4 55243245

Merge branch 'feature/iot' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into origin/feature/iot

Showing 15 changed files with 508 additions and 353 deletions
yudao-dependencies/pom.xml
@@ -626,6 +626,12 @@ @@ -626,6 +626,12 @@
626 <artifactId>vertx-web</artifactId> 626 <artifactId>vertx-web</artifactId>
627 <version>${vertx.version}</version> 627 <version>${vertx.version}</version>
628 </dependency> 628 </dependency>
  629 + <!-- Vert.x MQTT 模块 -->
  630 + <dependency>
  631 + <groupId>io.vertx</groupId>
  632 + <artifactId>vertx-mqtt</artifactId>
  633 + <version>${vertx.version}</version>
  634 + </dependency>
629 635
630 </dependencies> 636 </dependencies>
631 </dependencyManagement> 637 </dependencyManagement>
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/config/MqttConfig.java deleted
1 -package cn.iocoder.yudao.module.iot.mqttrpc.config;  
2 -  
3 -import lombok.Data;  
4 -import org.springframework.boot.context.properties.ConfigurationProperties;  
5 -import org.springframework.context.annotation.Configuration;  
6 -  
7 -@Data  
8 -@Configuration  
9 -@ConfigurationProperties(prefix = "mqtt")  
10 -public class MqttConfig {  
11 - /**  
12 - * MQTT 代理地址  
13 - */  
14 - private String broker;  
15 -  
16 - /**  
17 - * MQTT 用户名  
18 - */  
19 - private String username;  
20 -  
21 - /**  
22 - * MQTT 密码  
23 - */  
24 - private String password;  
25 -  
26 - /**  
27 - * MQTT 客户端 ID  
28 - */  
29 - private String clientId;  
30 -  
31 - /**  
32 - * MQTT 请求主题  
33 - */  
34 - private String requestTopic;  
35 -  
36 - /**  
37 - * MQTT 响应主题前缀  
38 - */  
39 - private String responseTopicPrefix;  
40 -}  
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/server/RpcServer.java deleted
1 -package cn.iocoder.yudao.module.iot.mqttrpc.server;  
2 -  
3 -import cn.hutool.core.lang.UUID;  
4 -import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcRequest;  
5 -import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcResponse;  
6 -import cn.iocoder.yudao.module.iot.mqttrpc.common.SerializationUtils;  
7 -import cn.iocoder.yudao.module.iot.mqttrpc.config.MqttConfig;  
8 -import lombok.extern.slf4j.Slf4j;  
9 -import org.eclipse.paho.client.mqttv3.*;  
10 -import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;  
11 -import org.springframework.stereotype.Service;  
12 -  
13 -import javax.annotation.PostConstruct;  
14 -import javax.annotation.PreDestroy;  
15 -import java.util.HashMap;  
16 -import java.util.Map;  
17 -  
18 -// TODO @芋艿:server 逻辑,再瞅瞅;  
19 -// TODO @haohao:如果只写在 iot biz 里,那么后续 server => client 貌似不方便?微信再讨论下~;  
20 -@Service  
21 -@Slf4j  
22 -public class RpcServer {  
23 -  
24 - private final MqttConfig mqttConfig;  
25 - private final MqttClient mqttClient;  
26 - private final Map<String, MethodInvoker> methodRegistry = new HashMap<>();  
27 -  
28 - public RpcServer(MqttConfig mqttConfig) throws MqttException {  
29 - this.mqttConfig = mqttConfig;  
30 - this.mqttClient = new MqttClient(mqttConfig.getBroker(), "rpc-server-" + UUID.randomUUID(), new MemoryPersistence());  
31 - MqttConnectOptions options = new MqttConnectOptions();  
32 - options.setAutomaticReconnect(true);  
33 - options.setCleanSession(true);  
34 - options.setUserName(mqttConfig.getUsername());  
35 - options.setPassword(mqttConfig.getPassword().toCharArray());  
36 - this.mqttClient.connect(options);  
37 - }  
38 -  
39 - @PostConstruct  
40 - public void init() throws MqttException {  
41 - mqttClient.subscribe(mqttConfig.getRequestTopic(), this::handleRequest);  
42 - log.info("RPC Server subscribed to topic: {}", mqttConfig.getRequestTopic());  
43 - }  
44 -  
45 - private void handleRequest(String topic, MqttMessage message) {  
46 - RpcRequest request = SerializationUtils.deserialize(new String(message.getPayload()), RpcRequest.class);  
47 - RpcResponse response = new RpcResponse();  
48 - response.setCorrelationId(request.getCorrelationId());  
49 -  
50 - try {  
51 - MethodInvoker invoker = methodRegistry.get(request.getMethod());  
52 - if (invoker == null) {  
53 - throw new NoSuchMethodException("Unknown method: " + request.getMethod());  
54 - }  
55 - Object result = invoker.invoke(request.getParams());  
56 - response.setResult(result);  
57 - } catch (Exception e) {  
58 - response.setError(e.getMessage());  
59 - log.error("Error processing RPC request: {}", e.getMessage(), e);  
60 - }  
61 -  
62 - String replyPayload = SerializationUtils.serialize(response);  
63 - MqttMessage replyMessage = new MqttMessage(replyPayload.getBytes());  
64 - replyMessage.setQos(1);  
65 - try {  
66 - mqttClient.publish(request.getReplyTo(), replyMessage);  
67 - log.info("Published response to {}", request.getReplyTo());  
68 - } catch (MqttException e) {  
69 - log.error("Failed to publish response: {}", e.getMessage(), e);  
70 - }  
71 - }  
72 -  
73 - /**  
74 - * 注册可调用的方法  
75 - *  
76 - * @param methodName 方法名称  
77 - * @param invoker 方法调用器  
78 - */  
79 - public void registerMethod(String methodName, MethodInvoker invoker) {  
80 - methodRegistry.put(methodName, invoker);  
81 - log.info("Registered method: {}", methodName);  
82 - }  
83 -  
84 - @PreDestroy  
85 - public void cleanup() throws MqttException {  
86 - mqttClient.disconnect();  
87 - log.info("RPC Server disconnected");  
88 - }  
89 -  
90 - /**  
91 - * 方法调用器接口  
92 - */  
93 - @FunctionalInterface  
94 - public interface MethodInvoker {  
95 -  
96 - Object invoke(Object[] params) throws Exception;  
97 -  
98 - }  
99 -  
100 -}  
101 \ No newline at end of file 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/plugin.properties 0 → 100644
  1 +plugin.id=emqx-plugin
  2 +plugin.class=cn.iocoder.yudao.module.iot.plugin.EmqxPlugin
  3 +plugin.version=0.0.1
  4 +plugin.provider=ahh
  5 +plugin.dependencies=
  6 +plugin.description=emqx-plugin-0.0.1
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3 + xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="
  4 + http://maven.apache.org/POM/4.0.0
  5 + http://maven.apache.org/xsd/maven-4.0.0.xsd">
  6 + <parent>
  7 + <artifactId>yudao-module-iot-plugin</artifactId>
  8 + <groupId>cn.iocoder.boot</groupId>
  9 + <version>${revision}</version>
  10 + </parent>
  11 + <modelVersion>4.0.0</modelVersion>
  12 + <packaging>jar</packaging>
  13 +
  14 + <artifactId>yudao-module-iot-emqx-plugin</artifactId>
  15 +
  16 + <name>${project.artifactId}</name>
  17 + <description>
  18 + 物联网 插件模块 - emqx 插件
  19 + </description>
  20 +
  21 + <properties>
  22 + <!-- 插件相关 -->
  23 + <plugin.id>emqx-plugin</plugin.id>
  24 + <plugin.class>cn.iocoder.yudao.module.iot.plugin.EmqxPlugin</plugin.class>
  25 + <plugin.version>0.0.1</plugin.version>
  26 + <plugin.provider>ahh</plugin.provider>
  27 + <plugin.description>emqx-plugin-0.0.1</plugin.description>
  28 + <plugin.dependencies/>
  29 + </properties>
  30 +
  31 + <build>
  32 + <plugins>
  33 + <!-- DOESN'T WORK WITH MAVEN 3 (I defined the plugin metadata in properties section)
  34 + <plugin>
  35 + <groupId>org.codehaus.mojo</groupId>
  36 + <artifactId>properties-maven-plugin</artifactId>
  37 + <version>1.0-alpha-2</version>
  38 + <executions>
  39 + <execution>
  40 + <phase>initialize</phase>
  41 + <goals>
  42 + <goal>read-project-properties</goal>
  43 + </goals>
  44 + <configuration>
  45 + <files>
  46 + <file>plugin.properties</file>
  47 + </files>
  48 + </configuration>
  49 + </execution>
  50 + </executions>
  51 + </plugin>
  52 + -->
  53 +
  54 + <plugin>
  55 + <groupId>org.apache.maven.plugins</groupId>
  56 + <artifactId>maven-antrun-plugin</artifactId>
  57 + <version>1.6</version>
  58 + <executions>
  59 + <execution>
  60 + <id>unzip jar file</id>
  61 + <phase>package</phase>
  62 + <configuration>
  63 + <target>
  64 + <unzip src="target/${project.artifactId}-${project.version}.${project.packaging}"
  65 + dest="target/plugin-classes"/>
  66 + </target>
  67 + </configuration>
  68 + <goals>
  69 + <goal>run</goal>
  70 + </goals>
  71 + </execution>
  72 + </executions>
  73 + </plugin>
  74 +
  75 + <plugin>
  76 + <artifactId>maven-assembly-plugin</artifactId>
  77 + <version>2.3</version>
  78 + <configuration>
  79 + <descriptors>
  80 + <descriptor>
  81 + src/main/assembly/assembly.xml
  82 + </descriptor>
  83 + </descriptors>
  84 + <appendAssemblyId>false</appendAssemblyId>
  85 + </configuration>
  86 + <executions>
  87 + <execution>
  88 + <id>make-assembly</id>
  89 + <phase>package</phase>
  90 + <goals>
  91 + <goal>attached</goal>
  92 + </goals>
  93 + </execution>
  94 + </executions>
  95 + </plugin>
  96 +
  97 + <plugin>
  98 + <groupId>org.apache.maven.plugins</groupId>
  99 + <artifactId>maven-jar-plugin</artifactId>
  100 + <version>2.4</version>
  101 + <configuration>
  102 + <archive>
  103 + <manifestEntries>
  104 + <Plugin-Id>${plugin.id}</Plugin-Id>
  105 + <Plugin-Class>${plugin.class}</Plugin-Class>
  106 + <Plugin-Version>${plugin.version}</Plugin-Version>
  107 + <Plugin-Provider>${plugin.provider}</Plugin-Provider>
  108 + <Plugin-Description>${plugin.description}</Plugin-Description>
  109 + <Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
  110 + </manifestEntries>
  111 + </archive>
  112 + </configuration>
  113 + </plugin>
  114 +
  115 + <plugin>
  116 + <artifactId>maven-deploy-plugin</artifactId>
  117 + <configuration>
  118 + <skip>true</skip>
  119 + </configuration>
  120 + </plugin>
  121 + </plugins>
  122 + </build>
  123 +
  124 + <dependencies>
  125 + <!-- 其他依赖项 -->
  126 + <dependency>
  127 + <groupId>org.springframework.boot</groupId>
  128 + <artifactId>spring-boot-starter-web</artifactId>
  129 + </dependency>
  130 + <!-- PF4J Spring 集成 -->
  131 + <dependency>
  132 + <groupId>org.pf4j</groupId>
  133 + <artifactId>pf4j-spring</artifactId>
  134 + <scope>provided</scope>
  135 + </dependency>
  136 + <!-- 项目依赖 -->
  137 + <dependency>
  138 + <groupId>cn.iocoder.boot</groupId>
  139 + <artifactId>yudao-module-iot-api</artifactId>
  140 + <version>${revision}</version>
  141 + </dependency>
  142 + <dependency>
  143 + <groupId>org.projectlombok</groupId>
  144 + <artifactId>lombok</artifactId>
  145 + <version>${lombok.version}</version>
  146 + <scope>provided</scope>
  147 + </dependency>
  148 + <!-- Vert.x 核心依赖 -->
  149 + <dependency>
  150 + <groupId>io.vertx</groupId>
  151 + <artifactId>vertx-core</artifactId>
  152 + </dependency>
  153 + <!-- Vert.x Web 模块 -->
  154 + <dependency>
  155 + <groupId>io.vertx</groupId>
  156 + <artifactId>vertx-web</artifactId>
  157 + </dependency>
  158 + <!-- MQTT -->
  159 + <dependency>
  160 + <groupId>org.eclipse.paho</groupId>
  161 + <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
  162 + </dependency>
  163 + </dependencies>
  164 +</project>
0 \ No newline at end of file 165 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/src/main/assembly/assembly.xml 0 → 100644
  1 +<assembly>
  2 + <id>plugin</id>
  3 + <formats>
  4 + <format>zip</format>
  5 + </formats>
  6 + <includeBaseDirectory>false</includeBaseDirectory>
  7 + <dependencySets>
  8 + <dependencySet>
  9 + <useProjectArtifact>false</useProjectArtifact>
  10 + <scope>runtime</scope>
  11 + <outputDirectory>lib</outputDirectory>
  12 + <includes>
  13 + <include>*:jar:*</include>
  14 + </includes>
  15 + </dependencySet>
  16 + </dependencySets>
  17 + <!--
  18 + <fileSets>
  19 + <fileSet>
  20 + <directory>target/classes</directory>
  21 + <outputDirectory>classes</outputDirectory>
  22 + </fileSet>
  23 + </fileSets>
  24 + -->
  25 + <fileSets>
  26 + <fileSet>
  27 + <directory>target/plugin-classes</directory>
  28 + <outputDirectory>classes</outputDirectory>
  29 + </fileSet>
  30 + </fileSets>
  31 +</assembly>
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/EmqxPlugin.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.plugin;
  2 +
  3 +import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
  4 +import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.pf4j.Plugin;
  7 +import org.pf4j.PluginWrapper;
  8 +
  9 +import javax.annotation.Resource;
  10 +import java.util.concurrent.ExecutorService;
  11 +import java.util.concurrent.Executors;
  12 +
  13 +@Slf4j
  14 +public class EmqxPlugin extends Plugin {
  15 +
  16 + private ExecutorService executorService;
  17 + @Resource
  18 + private DeviceDataApi deviceDataApi;
  19 +
  20 + public EmqxPlugin(PluginWrapper wrapper) {
  21 + super(wrapper);
  22 + this.executorService = Executors.newSingleThreadExecutor();
  23 + }
  24 +
  25 + @Override
  26 + public void start() {
  27 + log.info("EmqxPlugin.start()");
  28 +
  29 + if (executorService.isShutdown() || executorService.isTerminated()) {
  30 + executorService = Executors.newSingleThreadExecutor();
  31 + }
  32 +
  33 + deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class);
  34 + if (deviceDataApi == null) {
  35 + log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!");
  36 + return;
  37 + }
  38 +
  39 + }
  40 +
  41 + @Override
  42 + public void stop() {
  43 + log.info("EmqxPlugin.stop()");
  44 + }
  45 +}
0 \ No newline at end of file 46 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/pom.xml
@@ -147,20 +147,11 @@ @@ -147,20 +147,11 @@
147 <version>${lombok.version}</version> 147 <version>${lombok.version}</version>
148 <scope>provided</scope> 148 <scope>provided</scope>
149 </dependency> 149 </dependency>
150 - <!-- Vert.x 核心依赖 -->  
151 - <dependency>  
152 - <groupId>io.vertx</groupId>  
153 - <artifactId>vertx-core</artifactId>  
154 - </dependency>  
155 - <!-- Vert.x Web 模块 --> 150 + <!-- Vert.x Web -->
156 <dependency> 151 <dependency>
157 <groupId>io.vertx</groupId> 152 <groupId>io.vertx</groupId>
158 <artifactId>vertx-web</artifactId> 153 <artifactId>vertx-web</artifactId>
159 - </dependency>  
160 - <!-- MQTT -->  
161 - <dependency>  
162 - <groupId>org.eclipse.paho</groupId>  
163 - <artifactId>org.eclipse.paho.client.mqttv3</artifactId> 154 + <version>4.5.11</version>
164 </dependency> 155 </dependency>
165 </dependencies> 156 </dependencies>
166 </project> 157 </project>
167 \ No newline at end of file 158 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/controller/RpcController.java deleted
1 -  
2 -package cn.iocoder.yudao.module.iot.controller;  
3 -  
4 -import cn.iocoder.yudao.module.iot.mqttrpc.client.RpcClient;  
5 -import lombok.RequiredArgsConstructor;  
6 -import org.springframework.web.bind.annotation.PostMapping;  
7 -import org.springframework.web.bind.annotation.RequestMapping;  
8 -import org.springframework.web.bind.annotation.RequestParam;  
9 -import org.springframework.web.bind.annotation.RestController;  
10 -  
11 -import javax.annotation.Resource;  
12 -import java.util.concurrent.CompletableFuture;  
13 -  
14 -// TODO 芋艿:后续 review 下  
15 -/**  
16 - * 插件实例 RPC 接口  
17 - *  
18 - * @author 芋道源码  
19 - */  
20 -@RestController  
21 -@RequestMapping("/rpc")  
22 -@RequiredArgsConstructor  
23 -public class RpcController {  
24 -  
25 - @Resource  
26 - private RpcClient rpcClient;  
27 -  
28 - @PostMapping("/add")  
29 - public CompletableFuture<Object> add(@RequestParam int a, @RequestParam int b) throws Exception {  
30 - return rpcClient.call("add", new Object[]{a, b}, 10);  
31 - }  
32 -  
33 - @PostMapping("/concat")  
34 - public CompletableFuture<Object> concat(@RequestParam String str1, @RequestParam String str2) throws Exception {  
35 - return rpcClient.call("concat", new Object[]{str1, str2}, 10);  
36 - }  
37 -  
38 -}  
39 \ No newline at end of file 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/client/RpcClient.java deleted
1 -package cn.iocoder.yudao.module.iot.mqttrpc.client;  
2 -  
3 -import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcRequest;  
4 -import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcResponse;  
5 -import cn.iocoder.yudao.module.iot.mqttrpc.common.SerializationUtils;  
6 -import cn.iocoder.yudao.module.iot.mqttrpc.config.MqttConfig;  
7 -import lombok.extern.slf4j.Slf4j;  
8 -import org.eclipse.paho.client.mqttv3.MqttClient;  
9 -import org.eclipse.paho.client.mqttv3.MqttConnectOptions;  
10 -import org.eclipse.paho.client.mqttv3.MqttException;  
11 -import org.eclipse.paho.client.mqttv3.MqttMessage;  
12 -import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;  
13 -import org.springframework.stereotype.Service;  
14 -  
15 -import javax.annotation.PostConstruct;  
16 -import javax.annotation.PreDestroy;  
17 -import java.util.UUID;  
18 -import java.util.concurrent.*;  
19 -  
20 -// TODO @芋艿:需要考虑,怎么公用!  
21 -@Service  
22 -@Slf4j  
23 -public class RpcClient {  
24 -  
25 - private final MqttConfig mqttConfig;  
26 - private final MqttClient mqttClient;  
27 - private final ConcurrentMap<String, CompletableFuture<RpcResponse>> pendingRequests = new ConcurrentHashMap<>();  
28 - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);  
29 -  
30 - public RpcClient(MqttConfig mqttConfig) throws MqttException {  
31 - this.mqttConfig = mqttConfig;  
32 - this.mqttClient = new MqttClient(mqttConfig.getBroker(), mqttConfig.getClientId(), new MemoryPersistence());  
33 - MqttConnectOptions options = new MqttConnectOptions();  
34 - options.setAutomaticReconnect(true);  
35 - options.setCleanSession(true);  
36 - options.setUserName(mqttConfig.getUsername());  
37 - options.setPassword(mqttConfig.getPassword().toCharArray());  
38 - this.mqttClient.connect(options);  
39 - }  
40 -  
41 - @PostConstruct  
42 - public void init() throws MqttException {  
43 - mqttClient.subscribe(mqttConfig.getResponseTopicPrefix() + "#", this::handleResponse);  
44 - log.info("RPC Client subscribed to topics: {}", mqttConfig.getResponseTopicPrefix() + "#");  
45 - }  
46 -  
47 - private void handleResponse(String topic, MqttMessage message) {  
48 - String correlationId = topic.substring(mqttConfig.getResponseTopicPrefix().length());  
49 - RpcResponse response = SerializationUtils.deserialize(new String(message.getPayload()), RpcResponse.class);  
50 - CompletableFuture<RpcResponse> future = pendingRequests.remove(correlationId);  
51 - if (future != null) {  
52 - if (response.getError() != null) {  
53 - future.completeExceptionally(new RuntimeException(response.getError()));  
54 - } else {  
55 - future.complete(response);  
56 - }  
57 - } else {  
58 - log.warn("Received response for unknown correlationId: {}", correlationId);  
59 - }  
60 - }  
61 -  
62 - public CompletableFuture<Object> call(String method, Object[] params, int timeoutSeconds) throws MqttException {  
63 - String correlationId = UUID.randomUUID().toString();  
64 - String replyTo = mqttConfig.getResponseTopicPrefix() + correlationId;  
65 -  
66 - RpcRequest request = new RpcRequest(method, params, correlationId, replyTo);  
67 - String payload = SerializationUtils.serialize(request);  
68 - MqttMessage message = new MqttMessage(payload.getBytes());  
69 - message.setQos(1);  
70 - mqttClient.publish(mqttConfig.getRequestTopic(), message);  
71 -  
72 - CompletableFuture<RpcResponse> futureResponse = new CompletableFuture<>();  
73 - pendingRequests.put(correlationId, futureResponse);  
74 -  
75 - // 设置超时  
76 - scheduler.schedule(() -> {  
77 - CompletableFuture<RpcResponse> removed = pendingRequests.remove(correlationId);  
78 - if (removed != null) {  
79 - removed.completeExceptionally(new TimeoutException("RPC call timed out"));  
80 - }  
81 - }, timeoutSeconds, TimeUnit.SECONDS);  
82 -  
83 - // 返回最终的结果  
84 - return futureResponse.thenApply(RpcResponse::getResult);  
85 - }  
86 -  
87 - @PreDestroy  
88 - public void cleanup() throws MqttException {  
89 - mqttClient.disconnect();  
90 - scheduler.shutdown();  
91 - log.info("RPC Client disconnected");  
92 - }  
93 -}  
94 \ No newline at end of file 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/config/MqttConfig.java deleted
1 -package cn.iocoder.yudao.module.iot.mqttrpc.config;  
2 -  
3 -import lombok.Data;  
4 -import org.springframework.boot.context.properties.ConfigurationProperties;  
5 -import org.springframework.context.annotation.Configuration;  
6 -  
7 -@Data  
8 -@Configuration  
9 -@ConfigurationProperties(prefix = "mqtt")  
10 -public class MqttConfig {  
11 -  
12 - /**  
13 - * MQTT 代理地址  
14 - */  
15 - private String broker;  
16 -  
17 - /**  
18 - * MQTT 用户名  
19 - */  
20 - private String username;  
21 -  
22 - /**  
23 - * MQTT 密码  
24 - */  
25 - private String password;  
26 -  
27 - /**  
28 - * MQTT 客户端 ID  
29 - */  
30 - private String clientId;  
31 -  
32 - /**  
33 - * MQTT 请求主题  
34 - */  
35 - private String requestTopic;  
36 -  
37 - /**  
38 - * MQTT 响应主题前缀  
39 - */  
40 - private String responseTopicPrefix;  
41 -}  
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/plugin.properties
1 plugin.id=mqtt-plugin 1 plugin.id=mqtt-plugin
  2 +plugin.description=Vert.x MQTT plugin
2 plugin.class=cn.iocoder.yudao.module.iot.plugin.MqttPlugin 3 plugin.class=cn.iocoder.yudao.module.iot.plugin.MqttPlugin
3 -plugin.version=0.0.1 4 +plugin.version=1.0.0
  5 +plugin.requires=
4 plugin.provider=ahh 6 plugin.provider=ahh
5 -plugin.dependencies=  
6 -plugin.description=mqtt-plugin-0.0.1 7 +plugin.license=Apache-2.0
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/pom.xml
@@ -145,10 +145,11 @@ @@ -145,10 +145,11 @@
145 <version>${lombok.version}</version> 145 <version>${lombok.version}</version>
146 <scope>provided</scope> 146 <scope>provided</scope>
147 </dependency> 147 </dependency>
148 - <!-- MQTT --> 148 + <!-- Vert.x MQTT -->
149 <dependency> 149 <dependency>
150 - <groupId>org.eclipse.paho</groupId>  
151 - <artifactId>org.eclipse.paho.client.mqttv3</artifactId> 150 + <groupId>io.vertx</groupId>
  151 + <artifactId>vertx-mqtt</artifactId>
  152 + <version>4.5.11</version>
152 </dependency> 153 </dependency>
153 </dependencies> 154 </dependencies>
154 </project> 155 </project>
155 \ No newline at end of file 156 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java
1 package cn.iocoder.yudao.module.iot.plugin; 1 package cn.iocoder.yudao.module.iot.plugin;
2 2
3 -import cn.iocoder.yudao.module.iot.api.ServiceRegistry;  
4 -import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;  
5 import lombok.extern.slf4j.Slf4j; 3 import lombok.extern.slf4j.Slf4j;
6 import org.pf4j.Plugin; 4 import org.pf4j.Plugin;
7 import org.pf4j.PluginWrapper; 5 import org.pf4j.PluginWrapper;
8 6
9 -import javax.annotation.Resource;  
10 -import java.util.concurrent.ExecutorService;  
11 -import java.util.concurrent.Executors;  
12 -  
13 @Slf4j 7 @Slf4j
14 public class MqttPlugin extends Plugin { 8 public class MqttPlugin extends Plugin {
15 9
16 - private ExecutorService executorService;  
17 - @Resource  
18 - private DeviceDataApi deviceDataApi; 10 + private MqttServerExtension mqttServerExtension;
19 11
20 public MqttPlugin(PluginWrapper wrapper) { 12 public MqttPlugin(PluginWrapper wrapper) {
21 super(wrapper); 13 super(wrapper);
22 - this.executorService = Executors.newSingleThreadExecutor();  
23 } 14 }
24 15
25 @Override 16 @Override
26 public void start() { 17 public void start() {
27 - log.info("MqttPlugin.start()");  
28 -  
29 - if (executorService.isShutdown() || executorService.isTerminated()) {  
30 - executorService = Executors.newSingleThreadExecutor();  
31 - }  
32 -  
33 - deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class);  
34 - if (deviceDataApi == null) {  
35 - log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!");  
36 - return;  
37 - }  
38 - 18 + log.info("MQTT Plugin started.");
  19 + mqttServerExtension = new MqttServerExtension();
  20 + mqttServerExtension.startMqttServer();
39 } 21 }
40 22
41 @Override 23 @Override
42 public void stop() { 24 public void stop() {
43 - log.info("MqttPlugin.stop()"); 25 + log.info("MQTT Plugin stopped.");
  26 + if (mqttServerExtension != null) {
  27 + mqttServerExtension.stopMqttServer().onComplete(ar -> {
  28 + if (ar.succeeded()) {
  29 + log.info("Stopped MQTT Server successfully");
  30 + } else {
  31 + log.error("Failed to stop MQTT Server: {}", ar.cause().getMessage());
  32 + }
  33 + });
  34 + }
44 } 35 }
45 -}  
46 \ No newline at end of file 36 \ No newline at end of file
  37 +}
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttServerExtension.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.plugin;
  2 +
  3 +import io.netty.handler.codec.mqtt.MqttProperties;
  4 +import io.netty.handler.codec.mqtt.MqttQoS;
  5 +import io.vertx.core.Future;
  6 +import io.vertx.core.Vertx;
  7 +import io.vertx.core.buffer.Buffer;
  8 +import io.vertx.mqtt.MqttEndpoint;
  9 +import io.vertx.mqtt.MqttServer;
  10 +import io.vertx.mqtt.MqttServerOptions;
  11 +import io.vertx.mqtt.MqttTopicSubscription;
  12 +import io.vertx.mqtt.messages.MqttDisconnectMessage;
  13 +import io.vertx.mqtt.messages.MqttPublishMessage;
  14 +import io.vertx.mqtt.messages.MqttSubscribeMessage;
  15 +import io.vertx.mqtt.messages.MqttUnsubscribeMessage;
  16 +import io.vertx.mqtt.messages.codes.MqttSubAckReasonCode;
  17 +import lombok.extern.slf4j.Slf4j;
  18 +import org.pf4j.Extension;
  19 +
  20 +import java.nio.charset.Charset;
  21 +import java.util.ArrayList;
  22 +import java.util.List;
  23 +
  24 +/**
  25 + * 根据官方示例,整合常见 MQTT 功能到 PF4J 的 Extension 类中
  26 + */
  27 +@Slf4j
  28 +@Extension
  29 +public class MqttServerExtension {
  30 +
  31 + private Vertx vertx;
  32 + private MqttServer mqttServer;
  33 +
  34 + /**
  35 + * 启动 MQTT 服务端
  36 + * 可根据需要决定是否启用 SSL/TLS、WebSocket、多实例部署等
  37 + */
  38 + public void startMqttServer() {
  39 + // 初始化 Vert.x
  40 + vertx = Vertx.vertx();
  41 +
  42 + // ========== 如果需要 SSL/TLS,请参考下面注释,启用注释并替换端口、证书路径等 ==========
  43 + // MqttServerOptions options = new MqttServerOptions()
  44 + // .setPort(8883)
  45 + // .setKeyCertOptions(new PemKeyCertOptions()
  46 + // .setKeyPath("./src/test/resources/tls/server-key.pem")
  47 + // .setCertPath("./src/test/resources/tls/server-cert.pem"))
  48 + // .setSsl(true);
  49 +
  50 + // ========== 如果需要 WebSocket,请设置 setUseWebSocket(true) ==========
  51 + // options.setUseWebSocket(true);
  52 +
  53 + // ========== 默认不启用 SSL 的示例 ==========
  54 + MqttServerOptions options = new MqttServerOptions()
  55 + .setPort(1883)
  56 + .setHost("0.0.0.0")
  57 + .setUseWebSocket(false); // 如果需要 WebSocket,请改为 true
  58 +
  59 + mqttServer = MqttServer.create(vertx, options);
  60 +
  61 + // 指定 endpointHandler,处理客户端连接等
  62 + mqttServer.endpointHandler(endpoint -> {
  63 + handleClientConnect(endpoint);
  64 + handleDisconnect(endpoint);
  65 + handleSubscribe(endpoint);
  66 + handleUnsubscribe(endpoint);
  67 + handlePublish(endpoint);
  68 + handlePing(endpoint);
  69 + });
  70 +
  71 + // 启动监听
  72 + mqttServer.listen(ar -> {
  73 + if (ar.succeeded()) {
  74 + log.info("MQTT server is listening on port {}", mqttServer.actualPort());
  75 + } else {
  76 + log.error("Error on starting the server", ar.cause());
  77 + }
  78 + });
  79 + }
  80 +
  81 + /**
  82 + * 优雅关闭 MQTT 服务端
  83 + */
  84 + public Future<Void> stopMqttServer() {
  85 + if (mqttServer != null) {
  86 + return mqttServer.close().onComplete(ar -> {
  87 + if (ar.succeeded()) {
  88 + log.info("MQTT server closed.");
  89 + if (vertx != null) {
  90 + vertx.close();
  91 + log.info("Vert.x instance closed.");
  92 + }
  93 + } else {
  94 + log.error("Failed to close MQTT server: {}", ar.cause().getMessage());
  95 + }
  96 + });
  97 + }
  98 + return Future.succeededFuture();
  99 + }
  100 +
  101 + // ==================== 以下为官方示例中常见事件的处理封装 ====================
  102 +
  103 + /**
  104 + * 处理客户端连接 (CONNECT)
  105 + */
  106 + private void handleClientConnect(MqttEndpoint endpoint) {
  107 + // 打印 CONNECT 的主要信息
  108 + log.info("MQTT client [{}] request to connect, clean session = {}",
  109 + endpoint.clientIdentifier(), endpoint.isCleanSession());
  110 +
  111 + if (endpoint.auth() != null) {
  112 + log.info("[username = {}, password = {}]", endpoint.auth().getUsername(), endpoint.auth().getPassword());
  113 + }
  114 + log.info("[properties = {}]", endpoint.connectProperties());
  115 +
  116 + if (endpoint.will() != null) {
  117 + log.info("[will topic = {}, msg = {}, QoS = {}, isRetain = {}]",
  118 + endpoint.will().getWillTopic(),
  119 + new String(endpoint.will().getWillMessageBytes()),
  120 + endpoint.will().getWillQos(),
  121 + endpoint.will().isWillRetain());
  122 + }
  123 +
  124 + log.info("[keep alive timeout = {}]", endpoint.keepAliveTimeSeconds());
  125 +
  126 + // 接受远程客户端的连接
  127 + endpoint.accept(false);
  128 + }
  129 +
  130 + /**
  131 + * 处理客户端主动断开 (DISCONNECT)
  132 + */
  133 + private void handleDisconnect(MqttEndpoint endpoint) {
  134 + endpoint.disconnectMessageHandler((MqttDisconnectMessage disconnectMessage) -> {
  135 + log.info("Received disconnect from client [{}], reason code = {}",
  136 + endpoint.clientIdentifier(), disconnectMessage.code());
  137 + });
  138 + }
  139 +
  140 + /**
  141 + * 处理客户端订阅 (SUBSCRIBE)
  142 + */
  143 + private void handleSubscribe(MqttEndpoint endpoint) {
  144 + endpoint.subscribeHandler((MqttSubscribeMessage subscribe) -> {
  145 + List<MqttSubAckReasonCode> reasonCodes = new ArrayList<>();
  146 + for (MqttTopicSubscription s : subscribe.topicSubscriptions()) {
  147 + log.info("Subscription for {} with QoS {}", s.topicName(), s.qualityOfService());
  148 + // 将客户端请求的 QoS 转换为返回给客户端的 reason code(可能是错误码或实际 granted QoS)
  149 + reasonCodes.add(MqttSubAckReasonCode.qosGranted(s.qualityOfService()));
  150 + }
  151 + // 回复 SUBACK,MQTT 5.0 时可指定 reasonCodes、properties
  152 + endpoint.subscribeAcknowledge(subscribe.messageId(), reasonCodes, MqttProperties.NO_PROPERTIES);
  153 + });
  154 + }
  155 +
  156 + /**
  157 + * 处理客户端取消订阅 (UNSUBSCRIBE)
  158 + */
  159 + private void handleUnsubscribe(MqttEndpoint endpoint) {
  160 + endpoint.unsubscribeHandler((MqttUnsubscribeMessage unsubscribe) -> {
  161 + for (String topic : unsubscribe.topics()) {
  162 + log.info("Unsubscription for {}", topic);
  163 + }
  164 + // 回复 UNSUBACK,MQTT 5.0 时可指定 reasonCodes、properties
  165 + endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
  166 + });
  167 + }
  168 +
  169 + /**
  170 + * 处理客户端发布的消息 (PUBLISH)
  171 + */
  172 + private void handlePublish(MqttEndpoint endpoint) {
  173 + // 接收 PUBLISH 消息
  174 + endpoint.publishHandler((MqttPublishMessage message) -> {
  175 + String payload = message.payload().toString(Charset.defaultCharset());
  176 + log.info("Received message [{}] on topic [{}] with QoS [{}]",
  177 + payload, message.topicName(), message.qosLevel());
  178 +
  179 + // 根据不同 QoS,回复客户端
  180 + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
  181 + endpoint.publishAcknowledge(message.messageId());
  182 + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) {
  183 + endpoint.publishReceived(message.messageId());
  184 + }
  185 + });
  186 +
  187 + // 如果 QoS = 2,需要处理 PUBREL
  188 + endpoint.publishReleaseHandler(messageId -> {
  189 + endpoint.publishComplete(messageId);
  190 + });
  191 + }
  192 +
  193 + /**
  194 + * 处理客户端 PINGREQ
  195 + */
  196 + private void handlePing(MqttEndpoint endpoint) {
  197 + endpoint.pingHandler(v -> {
  198 + // 这里仅做日志, PINGRESP 已自动发送
  199 + log.info("Ping received from client [{}]", endpoint.clientIdentifier());
  200 + });
  201 + }
  202 +
  203 + // ==================== 如果需要服务端向客户端发布消息,可用以下示例 ====================
  204 +
  205 + /**
  206 + * 服务端主动向已连接的某个 endpoint 发布消息的示例
  207 + * 如果使用 MQTT 5.0,可以传递更多消息属性
  208 + */
  209 + public void publishToClient(MqttEndpoint endpoint, String topic, String content) {
  210 + endpoint.publish(topic,
  211 + Buffer.buffer(content),
  212 + MqttQoS.AT_LEAST_ONCE, // QoS 自行选择
  213 + false,
  214 + false);
  215 +
  216 + // 处理 QoS 1 和 QoS 2 的 ACK
  217 + endpoint.publishAcknowledgeHandler(messageId -> {
  218 + log.info("Received PUBACK from client [{}] for messageId = {}", endpoint.clientIdentifier(), messageId);
  219 + }).publishReceivedHandler(messageId -> {
  220 + endpoint.publishRelease(messageId);
  221 + }).publishCompletionHandler(messageId -> {
  222 + log.info("Received PUBCOMP from client [{}] for messageId = {}", endpoint.clientIdentifier(), messageId);
  223 + });
  224 + }
  225 +
  226 + // ==================== 如果需要多实例部署,用于多核扩展,可参考以下思路 ====================
  227 + // 例如,在宿主应用或插件中循环启动多个 MqttServerExtension 实例,或使用 Vert.x 的 deployVerticle:
  228 + // DeploymentOptions options = new DeploymentOptions().setInstances(10);
  229 + // vertx.deployVerticle(() -> new MyMqttVerticle(), options);
  230 +
  231 +}