最终效果如图


你可以通过 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并接线(和真机逻辑一致):
- 从左侧元件库搜索「LED」,拖到画布;
- 搜索「Resistor」(电阻),拖到画布(选220Ω);
- 接线:
- ESP32的
GPIO2引脚 → 电阻一端; - 电阻另一端 → LED的正极(长脚);
- LED的负极(短脚) → ESP32的
GND引脚。
- ESP32的
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:测试控制流程
- 确保Wokwi的仿真处于运行状态(串口监视器显示“已订阅主题”);
- 用浏览器打开你写的
esp32-control.html文件; - 点击网页上的「打开LED」按钮:
- 网页控制台会显示“已发送指令:ON”;
- Wokwi的串口监视器会显示“收到指令:ON → LED已打开”;
- Wokwi画布中的LED会点亮;
- 点击「关闭LED」按钮,LED会熄灭。
五、关键说明
- Wokwi的虚拟WiFi:Wokwi内置了“Wokwi-GUEST”虚拟WiFi,无需真实WiFi,代码中固定写死即可;
- 公共MQTT服务器:
broker.emqx.io是免费公共服务器,用于网页和虚拟ESP32的通信(绕开Wokwi的网络隔离); - MQTT端口:网页用
8083(WebSocket端口),ESP32用1883(TCP端口),这是EMQX公共服务器的配置要求; - 仿真持续运行:Wokwi页面关闭后仿真会停止,需保持页面打开才能控制。
通过这个方式,你就能用自己的网页界面,远程控制Wokwi里的虚拟ESP32 LED,完全不需要真机~
805

被折叠的 条评论
为什么被折叠?



