AI技术栈-智能控制系统的线上模拟

「鸿蒙心迹」“2025・领航者闯关记“主题征文活动 10w+人浏览 563人参与

最终效果如图
在这里插入图片描述在这里插入图片描述

你可以通过 Wokwi Online Simulator(目前最流行的在线ESP32仿真平台)实现“网页界面控制虚拟ESP32的LED”,核心逻辑是让Wokwi里的虚拟ESP32和你的网页通过公共MQTT服务器通信(绕开Wokwi的虚拟网络隔离)。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

为了开发便于调试的一种快速验证方式

一、工具准备

  • 在线仿真平台:Wokwi(免费,无需安装)→ https://wokwi.com
  • 公共MQTT服务器(用于网页和虚拟ESP32通信):EMQX公共服务器(免费)→ broker.emqx.io(端口1883)
  • 网页技术:HTML + JavaScript(用paho-mqtt库实现MQTT通信)

二、步骤1:在Wokwi创建“虚拟ESP32+LED”仿真项目

1. 打开Wokwi,新建ESP32项目
  • 访问 https://wokwi.com,点击右上角「New Project」→ 选择「ESP32」→ 点击「Create Project」。
2. 接线虚拟ESP32和LED

在Wokwi的画布中,添加LED并接线(和真机逻辑一致):

  1. 从左侧元件库搜索「LED」,拖到画布;
  2. 搜索「Resistor」(电阻),拖到画布(选220Ω);
  3. 接线:
    • ESP32的 GPIO2 引脚 → 电阻一端;
    • 电阻另一端 → LED的正极(长脚);
    • LED的负极(短脚) → ESP32的 GND 引脚。
3. 编写虚拟ESP32的代码(连接MQTT控制LED)

将Wokwi的代码编辑器内容替换为以下代码(连接公共MQTT服务器,订阅指令主题):

// --------------- 先包含必要头文件(确保WiFiClient类型被识别)---------------
#include <WiFi.h>

// --------------- 修正后的PubSubClient类(依赖WiFiClient)---------------
class PubSubClient {
private:
  WiFiClient* _client;          // 修正:明确WiFiClient类型
  const char* _domain;
  uint8_t _port;
  const char* _clientId;
  uint16_t _keepAlive;
  unsigned long _lastOutActivity;
  bool _connected;
  void (*_callback)(char*, uint8_t*, unsigned int);
  uint8_t _buffer[128];
  uint16_t _bufferSize = 128;
  uint16_t _bufferLength = 0;

  // 辅助函数:写入字符串到MQTT包
  uint16_t writeString(const char* string, uint8_t* buf, uint16_t pos) {
    uint16_t len = strlen(string);
    buf[pos++] = (len >> 8) & 0xFF;
    buf[pos++] = len & 0xFF;
    memcpy(buf + pos, string, len);
    return pos + len;
  }

public:
  // 构造函数:接收WiFiClient对象
  PubSubClient(WiFiClient& client) {
    _client = &client;
    _keepAlive = 15;
    _connected = false;
  }

  // 设置MQTT服务器
  void setServer(const char* domain, uint8_t port) {
    _domain = domain;
    _port = port;
  }

  // 设置消息回调函数
  void setCallback(void (*callback)(char*, uint8_t*, unsigned int)) {
    _callback = callback;
  }

  // 连接MQTT服务器
  bool connect(const char* clientId) {
    _clientId = clientId;
    // 连接MQTT服务器(通过WiFiClient)
    if (!_client->connect(_domain, _port)) {
      return false;
    }

    // 构造MQTT CONNECT数据包
    _bufferLength = 0;
    _buffer[_bufferLength++] = 0x10; // 消息类型:CONNECT
    _buffer[_bufferLength++] = 0;    // 剩余长度(占位)
    uint16_t lenPos = _bufferLength;
    _bufferLength += 2;

    // 写入MQTT协议名(MQTT 3.1.1)
    _buffer[_bufferLength++] = 0x00;
    _buffer[_bufferLength++] = 0x04;
    _buffer[_bufferLength++] = 'M';
    _buffer[_bufferLength++] = 'Q';
    _buffer[_bufferLength++] = 'T';
    _buffer[_bufferLength++] = 'T';
    _buffer[_bufferLength++] = 0x04; // 协议版本:3.1.1

    // 写入连接标志(清理会话)
    _buffer[_bufferLength++] = 0x02;
    // 写入KeepAlive时间(15秒)
    _buffer[_bufferLength++] = 0x00;
    _buffer[_bufferLength++] = 0x0F;

    // 写入客户端ID
    writeString(_clientId, _buffer, _bufferLength);

    // 填充剩余长度
    uint16_t totalLen = _bufferLength - lenPos - 2;
    _buffer[lenPos] = (totalLen >> 8) & 0xFF;
    _buffer[lenPos + 1] = totalLen & 0xFF;
    _buffer[1] = totalLen + 2;

    // 发送CONNECT包
    _client->write(_buffer, _bufferLength);
    _lastOutActivity = millis();

    // 等待CONNACK响应(4字节)
    while (_client->available() < 4) delay(10);
    _client->read(_buffer, 4);
    // 连接成功的标志是响应码为0
    _connected = (_buffer[3] == 0);
    return _connected;
  }

  // 订阅主题
  bool subscribe(const char* topic) {
    if (!_connected) return false;

    // 构造SUBSCRIBE数据包
    _bufferLength = 0;
    _buffer[_bufferLength++] = 0x80; // 消息类型:SUBSCRIBE
    _buffer[_bufferLength++] = 0;    // 剩余长度(占位)
    uint16_t lenPos = _bufferLength;
    _bufferLength += 2;

    // 写入Packet ID
    _buffer[_bufferLength++] = 0x00;
    _buffer[_bufferLength++] = 0x01;
    // 写入订阅主题
    writeString(topic, _buffer, _bufferLength);
    // 写入QoS等级(0)
    _buffer[_bufferLength++] = 0x00;

    // 填充剩余长度
    uint16_t totalLen = _bufferLength - lenPos - 2;
    _buffer[lenPos] = (totalLen >> 8) & 0xFF;
    _buffer[lenPos + 1] = totalLen & 0xFF;
    _buffer[1] = totalLen + 2;

    // 发送SUBSCRIBE包
    _client->write(_buffer, _bufferLength);
    return true;
  }

  // 处理MQTT消息循环
  bool loop() {
    if (!_client->connected()) {
      _connected = false;
      return false;
    }

    // 读取收到的消息
    if (_client->available()) {
      _bufferLength = _client->read(_buffer, _bufferSize);
      // 判断是否是PUBLISH消息
      if ((_buffer[0] & 0xF0) == 0x30) {
        uint16_t topicLen = (_buffer[2] << 8) | _buffer[3];
        char* topic = (char*)&_buffer[4];
        uint8_t* payload = &_buffer[4 + topicLen];
        unsigned int payloadLen = _bufferLength - 4 - topicLen;
        // 调用回调函数处理消息
        if (_callback) _callback(topic, payload, payloadLen);
      }
      _bufferLength = 0;
    }

    // 发送PINGREQ保持连接
    if (millis() - _lastOutActivity > _keepAlive * 1000) {
      _buffer[0] = 0xC0; // 消息类型:PINGREQ
      _buffer[1] = 0x00; // 剩余长度
      _client->write(_buffer, 2);
      _lastOutActivity = millis();
    }
    return true;
  }

  // 判断是否已连接
  bool connected() {
    return _connected;
  }
};

// --------------- ESP32控制逻辑 ---------------
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PWD = "";
// 替换为test.mosquitto.org(TCP端口1883)
const char* MQTT_BROKER = "test.mosquitto.org";
const uint8_t MQTT_PORT = 1883;
const char* MQTT_TOPIC = "wokwi/esp32/led";  // 保持和网页一致
const int LED_PIN = 2;

// 创建WiFiClient和PubSubClient实例
WiFiClient espClient;
PubSubClient mqttClient(espClient);

// MQTT消息回调函数
void onMqttMessage(char* topic, uint8_t* payload, unsigned int length) {
  String cmd = "";
  for (int i = 0; i < length; i++) {
    cmd += (char)payload[i];
  }
  cmd.trim();
  Serial.println("收到指令:" + cmd);
  // 控制LED
  digitalWrite(LED_PIN, cmd.equals("ON") ? HIGH : LOW);
}

// 连接WiFi
void connectWiFi() {
  Serial.begin(115200);
  Serial.print("连接虚拟WiFi...");
  WiFi.begin(WIFI_SSID, WIFI_PWD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi连接成功!IP:" + WiFi.localIP().toString());
}

// 连接MQTT服务器
void connectMQTT() {
  while (!mqttClient.connected()) {
    Serial.print("连接MQTT服务器...");
    // 生成随机客户端ID
    String clientId = "Wokwi-ESP32-" + String(random(0xFFFF), HEX);
    if (mqttClient.connect(clientId.c_str())) {
      Serial.println("成功!");
      // 订阅指令主题
      mqttClient.subscribe(MQTT_TOPIC);
    } else {
      Serial.println("失败,5秒后重试...");
      delay(5000);
    }
  }
}

void setup() {
  // 初始化LED引脚
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  // 连接WiFi
  connectWiFi();
  // 配置MQTT服务器和回调
  mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
  mqttClient.setCallback(onMqttMessage);
}

void loop() {
  // 保持MQTT连接
  if (!mqttClient.connected()) {
    connectMQTT();
  }
  // 处理MQTT消息
  mqttClient.loop();
}
4. 运行Wokwi仿真

点击Wokwi顶部的「Start Simulation」按钮(三角形播放图标),此时:

  • 底部串口监视器会显示“虚拟WiFi连接成功”→“MQTT连接成功”→“已订阅主题”;
  • 保持Wokwi页面处于打开状态(仿真需持续运行)。

三、步骤2:编写你的网页界面(控制虚拟ESP32)

创建一个HTML文件(比如esp32-control.html),内容如下(通过MQTT发布指令控制Wokwi里的LED):

<!DOCTYPE html>
<html>
<head>
  <title>网页控制虚拟ESP32</title>
  <!-- 引入MQTT客户端库(paho-mqtt) -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.1.0/paho-mqtt.min.js"></script>
  <style>
    button { padding: 10px 20px; font-size: 16px; margin: 5px; }
  </style>
</head>
<body>
  <h1>控制虚拟ESP32的LED</h1>
  <button onclick="sendCmd('ON')">打开LED</button>
  <button onclick="sendCmd('OFF')">关闭LED</button>

  <script>
    // -------------------------- 和Wokwi一致的MQTT配置 --------------------------
    const MQTT_BROKER = "broker.emqx.io";
    const MQTT_PORT = 8083;  // EMQX公共服务器的WebSocket端口(非1883)
    const MQTT_TOPIC = "wokwi/esp32/led";

    // 创建MQTT客户端
    const client = new Paho.MQTT.Client(MQTT_BROKER, MQTT_PORT, "Web-Client-" + Math.random().toString(16).substr(2, 8));

    // 连接MQTT服务器
    client.connect({
      onSuccess: function() {
        console.log("网页已连接MQTT服务器");
      },
      onFailure: function(error) {
        console.error("MQTT连接失败:", error);
      }
    });

    // 发送指令到MQTT主题
    function sendCmd(cmd) {
      const message = new Paho.MQTT.Message(cmd);
      message.destinationName = MQTT_TOPIC;
      client.send(message);
      console.log("已发送指令:" + cmd);
    }
  </script>
</body>
</html>
package com.smart.device.control.interfaces.controller;

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.util.Scanner;

/**
 * ESP32 LED MQTT控制类
 * 适配场景:1. 本地EMQX服务器  2. 公共MQTT服务器(test.mosquitto.org)
 */
public class MqttLedController {
    // -------------------------- 可配置项(根据实际场景选择)--------------------------
    // 场景1:连接本地EMQX服务器(推荐,和ESP32真机/本地仿真通信)
//    private static final String MQTT_BROKER = "tcp://192.168.1.3:1883";  // 替换为你的电脑本地IP
    // 场景2:连接公共MQTT服务器(适配Wokwi虚拟ESP32,注释场景1,打开下面注释)
     private static final String MQTT_BROKER = "tcp://test.mosquitto.org:1883";

    private static final String MQTT_TOPIC = "smart_led/control";  // 和ESP32订阅主题一致
    private static final String CLIENT_ID = "Java_Client_" + System.currentTimeMillis();
    private static MqttClient mqttClient;  // 全局客户端对象,方便资源释放
    private static Scanner scanner;        // 全局扫描器,防止重复创建

    public static void main(String[] args) {
        try {
            // 1. 初始化扫描器(全局)
            scanner = new Scanner(System.in);

            // 2. 初始化MQTT客户端(内存持久化,避免文件残留)
            mqttClient = new MqttClient(MQTT_BROKER, CLIENT_ID, new MemoryPersistence());

            // 3. 配置连接参数(增强稳定性)
            MqttConnectOptions connOpts = new MqttConnectOptions();
            connOpts.setCleanSession(true);        // 清除旧会话,避免重复消息
            connOpts.setConnectionTimeout(15);     // 延长连接超时(15秒,适配网络波动)
            connOpts.setKeepAliveInterval(60);     // 心跳间隔60秒
            connOpts.setAutomaticReconnect(true);  // 开启自动重连(核心修复:断网后自动恢复)
            connOpts.setMaxInflight(10);           // 最大并发消息数,防止阻塞

            // 4. 注册连接丢失回调(核心修复:感知连接状态)
            mqttClient.setCallback(new MqttCallback() {
                @Override
                public void connectionLost(Throwable cause) {
                    System.err.println("\n【警告】MQTT连接丢失:" + cause.getMessage());
                    System.err.println("正在尝试自动重连...");
                }

                @Override
                public void messageArrived(String topic, MqttMessage message) {
                    // 可选:监听ESP32的状态反馈(如果ESP32有上报)
                    System.out.println("\n收到设备反馈:主题=" + topic + ",内容=" + new String(message.getPayload()));
                }

                @Override
                public void deliveryComplete(IMqttDeliveryToken token) {
                    // 确认消息已送达(QoS=1时生效)
                    System.out.println("指令已确认送达:" + token.getMessageId());
                }
            });

            // 5. 连接MQTT服务器(增加状态提示)
            System.out.println("=== ESP32 LED MQTT控制器 ===");
            System.out.println("正在连接MQTT服务器:" + MQTT_BROKER);
            mqttClient.connect(connOpts);
            System.out.println("✅ MQTT连接成功!客户端ID:" + CLIENT_ID);
            System.out.println("📌 控制主题:" + MQTT_TOPIC);
            System.out.println("----------------------------");
            System.out.println("输入ON打开LED,输入OFF关闭LED(输入exit退出):");

            // 6. 控制台指令循环(修复空输入、无效输入问题)
            String cmd;
            while (true) {
                cmd = scanner.nextLine().trim();

                // 退出逻辑
                if (cmd.equalsIgnoreCase("exit") || cmd.equalsIgnoreCase("quit")) {
                    System.out.println("🚪 准备退出控制器...");
                    break;
                }

                // 空输入处理
                if (cmd.isEmpty()) {
                    System.out.println("❌ 指令不能为空!请输入ON/OFF/exit");
                    continue;
                }

                // 有效指令处理
                if (cmd.equals("ON") || cmd.equals("OFF")) {
                    MqttMessage message = new MqttMessage(cmd.getBytes());
                    message.setQos(1);          // QoS=1:确保至少送达一次
                    message.setRetained(false); // 不保留消息,避免设备重启后误执行

                    // 发送指令
                    mqttClient.publish(MQTT_TOPIC, message);
                    System.out.println("✅ 已发送指令:" + cmd);
                } else {
                    System.out.println("❌ 无效指令!仅支持:ON(打开)、OFF(关闭)、exit(退出)");
                }
            }

        } catch (MqttException e) {
            // 细化MQTT异常处理(核心修复:明确错误原因)
            System.err.println("\n❌ MQTT操作失败!错误码:" + e.getReasonCode());
            switch (e.getReasonCode()) {
                case MqttException.REASON_CODE_CONNECTION_LOST:
                    System.err.println("原因:连接丢失,请检查网络或服务器地址");
                    break;
                case MqttException.REASON_CODE_CLIENT_NOT_CONNECTED:
                    System.err.println("原因:客户端未连接,请确认服务器地址正确");
                    break;
                case MqttException.REASON_CODE_BROKER_UNAVAILABLE:
                    System.err.println("原因:MQTT服务器不可用,请检查服务器是否启动");
                    break;
                default:
                    System.err.println("原因:" + e.getMessage());
            }
            e.printStackTrace();
        } catch (Exception e) {
            // 捕获其他异常(如IO异常)
            System.err.println("\n❌ 程序异常:" + e.getMessage());
            e.printStackTrace();
        } finally {
            // 核心修复:优雅释放资源(无论是否异常,都关闭)
            try {
                if (mqttClient != null && mqttClient.isConnected()) {
                    mqttClient.disconnect();
                    System.out.println("✅ MQTT连接已断开");
                }
                if (scanner != null) {
                    scanner.close();
                    System.out.println("✅ 控制台输入已关闭");
                }
            } catch (MqttException e) {
                System.err.println("❌ 断开连接失败:" + e.getMessage());
            }
            System.out.println("=== 控制器已退出 ===");
        }
    }
}

四、步骤3:测试控制流程

  1. 确保Wokwi的仿真处于运行状态(串口监视器显示“已订阅主题”);
  2. 用浏览器打开你写的esp32-control.html文件;
  3. 点击网页上的「打开LED」按钮:
    • 网页控制台会显示“已发送指令:ON”;
    • Wokwi的串口监视器会显示“收到指令:ON → LED已打开”;
    • Wokwi画布中的LED会点亮
  4. 点击「关闭LED」按钮,LED会熄灭

五、关键说明

  1. Wokwi的虚拟WiFi:Wokwi内置了“Wokwi-GUEST”虚拟WiFi,无需真实WiFi,代码中固定写死即可;
  2. 公共MQTT服务器broker.emqx.io是免费公共服务器,用于网页和虚拟ESP32的通信(绕开Wokwi的网络隔离);
  3. MQTT端口:网页用8083(WebSocket端口),ESP32用1883(TCP端口),这是EMQX公共服务器的配置要求;
  4. 仿真持续运行:Wokwi页面关闭后仿真会停止,需保持页面打开才能控制。

通过这个方式,你就能用自己的网页界面,远程控制Wokwi里的虚拟ESP32 LED,完全不需要真机~

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Coder_Boy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值