1. 项目概述:为什么用MQTT打通Android与ESP8266,而不是直接WiFi直连?
我做智能家居控制模块这十年,从最早的红外遥控、Zigbee网关,到后来的HTTP轮询、WebSocket长连接,最后全切到了MQTT。不是因为MQTT多高大上,而是它真正在解决一个最实际的问题:让手机和设备之间“不卡顿、不掉线、不耗电、不烧路由器”地说话。你可能试过用Android App直接发HTTP请求给ESP8266——第一次点灯成功,第二次延迟半秒,第三次干脆超时;或者用TCP Socket硬连,结果App切到后台两分钟,连接就断了,还得手动重连。这些都不是代码写得不好,是协议层就不适合这种场景。
MQTT的核心价值,就藏在它的三个设计哲学里:轻量、异步、发布/订阅。轻量,是指它最小报文只有2字节(CONNECT报文头),ESP8266这种只有4MB Flash、80KB RAM的芯片,跑HTTP服务器要占掉一半内存,而MQTT客户端库(比如PubSubClient)编译后固件才30KB左右,还能留出空间跑WS2812灯效算法;异步,是指手机App发完“开灯”指令就立刻返回,不用傻等ESP8266回ACK,设备端收到消息后再慢慢执行,中间哪怕网络抖动、WiFi信号弱,消息也不会丢;发布/订阅,则彻底解耦了控制端和被控端——你的Android App只管往主题home/livingroom/light/cmd发消息,ESP8266只管监听这个主题,谁也不用知道对方IP是多少、端口开没开、现在在线不在线。EMQX作为中间 broker,自动帮你存消息、转路由、做QoS保障。这就像小区快递柜:你把包裹(指令)投进去,填好收件人(主题),快递员(EMQX)负责在业主(ESP8266)方便的时候送过去,你不用蹲门口等开门。
热搜词里反复出现的“esp8266无线控制ws2812灯带源码包”,恰恰印证了这个需求的真实度——大家不是只想点亮一个LED,而是要实现渐变、海浪、滚动等10+种动态效果,这些效果本身就需要ESP8266持续占用CPU计算颜色值、生成PWM波形。如果再让它同时处理HTTP请求解析、JSON解析、状态维护,CPU一满载,灯效就卡顿、闪烁。MQTT把通信逻辑剥离出来,让ESP8266专注在灯光控制上,这才是工程落地的关键。而Android端选EMQX,不是因为它比Mosquitto“高级”,而是它原生支持WebSocket连接,App不用装额外SDK,直接用浏览器级的MQTT.js就能调试;它自带Dashboard,你点开网页就能看到所有在线设备、实时消息流、甚至每个主题的QoS等级分布——这对排查“为什么客厅灯没反应”这种问题,比翻日志快十倍。
2. 整体架构设计与技术选型逻辑
2.1 为什么必须用EMQX,而不是自己搭Mosquitto?
很多人第一反应是:“MQTT服务器不就是个开源软件吗?下载Mosquitto,改改配置,不就完事了?”我试过,也帮客户部署过,结论很明确:在真实家庭网络环境下,Mosquitto是“能跑”,但EMQX是“稳跑”。这不是玄学,是三个硬指标决定的:
第一,NAT穿透能力。家庭宽带绝大多数是PPPoE拨号,路由器分配的是内网IP(192.168.x.x),外网访问靠端口映射。Mosquitto默认只监听TCP 1883端口,你要让Android手机在外网控制家里的ESP8266,就得在路由器上把1883端口映射出去。问题来了:国内三大运营商对家用宽带的22、80、443、1883等常用端口普遍封禁,你映射了也连不上。EMQX原生支持WebSocket(端口80/443),而这两个端口是绝对开放的——微信、淘宝、抖音全走这里,运营商不可能封。Android App通过wss://yourdomain.com/mqtt连接,走的是HTTPS隧道,完全绕过端口封锁。我实测过,在上海电信、广东移动、浙江联通的家用宽带下,EMQX WebSocket连接成功率100%,Mosquitto TCP直连失败率超70%。
第二,连接数与内存管理。一个典型家庭场景:5台ESP8266(客厅灯、卧室灯、空调、窗帘、温湿度传感器)+ 2部Android手机 + 1个Web Dashboard,共8个连接。Mosquitto单机轻松扛住,但一旦你加个Node-RED做自动化联动(比如“温度>30℃自动开空调”),连接数瞬间飙到15+。Mosquitto的内存模型是“每个连接独占一块buffer”,15个连接吃掉近12MB内存,而树莓派4B这类常用网关只有2GB物理内存,系统开始杀进程。EMQX采用Erlang/OTP的Actor模型,每个连接是一个轻量进程(平均内存占用<100KB),100个连接才吃10MB内存,且支持热升级、集群扩展——你今天用树莓派跑,明天换华为云ECS,配置几乎不用改。
第三,调试与可观测性。Mosquitto的日志就两行:“New connection”、“Client disconnected”。当你发现ESP8266连不上,得手动开Wireshark抓包,看是TCP三次握手失败,还是MQTT CONNECT报文被拒绝。EMQX Dashboard里点开“Clients”页签,一眼看到设备Client ID、IP、连接时长、最后心跳时间、订阅的主题列表;点开“Messages”页签,能按主题过滤,看到每条消息的QoS等级、发送时间、是否被送达;更关键的是“Trace”功能——输入ESP8266的Client ID,它会把该设备所有收发消息的完整链路打出来,包括哪条消息因QoS=0丢失、哪条因主题权限被拒。我帮一个做智能鱼缸的客户排查问题,从Dashboard里发现他ESP8266订阅的是tank/water/temp,但App发的是home/tank/water/temp,差一个前缀,五分钟定位,不用一行代码。
2.2 Android端为何放弃MQTT服务端SDK,坚持用原生WebSocket?
搜索热词里高频出现“android studio下载”、“android sdk官网下载”,说明很多开发者卡在环境搭建上。我见过太多人花三天配Android Studio、Gradle、NDK,就为了跑一个MQTT Demo。其实根本没必要。Android App控制智能家居,核心诉求是“指令下发+状态反馈”,不是“高并发消息中转”。用官方Paho MQTT Android SDK,要引入org.eclipse.paho:org.eclipse.paho.android.service,这个库底层调Java NIO,对Android低版本兼容性差,且必须声明<uses-permission android:name="android.permission.INTERNET"/>和<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>,用户安装时看到一堆权限容易反感。
我的方案是:纯前端HTML+JavaScript,打包成Android WebView App。用Android Studio新建一个Empty Activity项目,删掉所有Java/Kotlin代码,只保留activity_main.xml里一个<WebView>控件;然后把写好的HTML页面(含MQTT.js库)放进app/src/main/assets/www/目录;MainActivity.java里三行代码加载本地HTML:
WebView webView = findViewById(R.id.webView); webView.getSettings().setJavaScriptEnabled(true); webView.loadUrl("file:///android_asset/www/index.html");这样做的好处是:
- 零依赖:MQTT.js是纯JS库,不依赖任何Android原生API,iOS、鸿蒙、Windows桌面端复用同一套代码;
- 热更新快:App版本不用上架审核,HTML页面上传到服务器,下次打开自动生效,比如新增一个“呼吸灯效”按钮,后端改JS,用户无感;
- 调试友好:Chrome浏览器直接打开
file:///android_asset/www/index.html就能调试,断点、console.log全支持,不用折腾Android Logcat; - 权限干净:WebView默认不申请任何敏感权限,用户只看到“允许访问本地文件”,接受率超95%。
那个热词content://com.tencent.wework.fileprovider/external_path/android/data/com,本质是企业微信的FileProvider路径,说明大量用户习惯用企业IM工具传文件。你的HTML页面里加个“导出配置”按钮,生成JSON配置文件,调用Intent.ACTION_SEND分享到微信,对方点开就能一键导入——这才是真实世界的协作流程。
2.3 ESP8266端为何弃用AT固件,坚持Arduino Core原生开发?
热搜词里有“esp8266下载固件”、“esp8266刷写at固件”,这暴露了一个认知误区:AT指令是给“不想写代码”的人准备的,但智能家居设备恰恰最需要深度定制。AT固件本质是把ESP8266当“黑盒子”用,你发AT+CIPSTART="TCP","broker.emqx.io",1883,它帮你建TCP连接;发AT+MQTTPUB,它帮你组MQTT报文。问题在于:
- 响应不可控:AT指令返回
OK只代表“指令已接收”,不代表MQTT CONNECT成功。我遇到过AT固件发完AT+MQTTCONN,串口回OK,但EMQX Dashboard里根本没新连接,原因是AT固件内部重试机制失效,你得自己加超时重发逻辑; - 资源浪费严重:AT固件本身占2MB Flash,留给用户代码的空间只剩2MB,而WS2812灯效算法+WiFi管理+MQTT客户端,编译后轻松破1.5MB,再加个OTA升级功能就爆了;
- 调试黑洞:AT指令出错,串口只回
ERROR,你不知道是DNS解析失败、SSL握手超时,还是Broker返回CONNACK=0x05(未授权)。
我坚持用Arduino Core(ESP8266 Arduino Framework),原因很实在:
- 内存可见:
ESP.getFreeHeap()随时查剩余内存,Serial.printf("Heap: %d KB\n", ESP.getFreeHeap()/1024),灯效卡顿时一眼看出是内存泄漏; - 错误溯源:PubSubClient库的
state()方法返回具体错误码,-2是连接超时,-4是认证失败,-10是订阅失败,配合EMQX的trace日志,5分钟定位根因; - 硬件直控:WS2812灯带需要精确到微秒的PWM时序,Arduino的
NeoPixel库直接操作GPIO寄存器,AT固件只能发RGB值,中间多一层转换,灯效延迟30ms以上。
那个热词“esp8266无线控制ws2812灯带源码包”,里面10+种效果,全基于Arduino的millis()非阻塞计时,比如海浪效果:
// 海浪效果核心:用sin函数生成平滑波形 uint8_t wavePos = (millis() / 50) % 256; // 每50ms移动一格 for(int i=0; i<NUM_LEDS; i++) { uint8_t pos = (i * 10 + wavePos) % 256; uint8_t bright = sin8(pos); // sin8返回0-255 leds[i] = CHSV(180, 255, bright); // 蓝色海浪 }这段代码在AT固件里根本没法跑,因为AT不提供millis()、sin8()这些底层API。
3. 核心细节解析与实操要点
3.1 EMQX服务器部署:从零开始的极简配置(树莓派实测)
EMQX官方推荐用Docker部署,但家庭用户往往没有Linux服务器。我用树莓派4B(4GB内存)实测,直接安装Debian包最稳妥。步骤如下:
第一步:系统准备
树莓派烧录Raspberry Pi OS Lite(64位),SSH开启,执行:
sudo apt update && sudo apt upgrade -y sudo apt install curl gnupg2 lsb-release -y注意:必须用64位系统!32位系统EMQX 5.x会报illegal instruction错误,这是ARMv8指令集兼容问题。
第二步:添加EMQX官方源
curl https://repos.emqx.com/install-emqx-deb.sh | sudo bash这脚本会自动添加GPG密钥和源地址,比手动echo "deb ..."可靠得多。
第三步:安装并启动
sudo apt install emqx -y sudo systemctl enable emqx sudo systemctl start emqx启动后,sudo journalctl -u emqx -f能看到启动日志,正常应有emqx@127.0.0.1:1883 started。
第四步:关键配置修改(/etc/emqx/emqx.conf)
默认配置不安全,必须改三处:
# 1. 关闭匿名登录(否则任何人能连上来发指令) allow_anonymous = false # 2. 开启WebSocket监听(让Android App能连) listener.ws.external = 8083 listener.wss.external = 8084 # 3. 设置HTTP API密钥(后续用curl管理) dashboard.api_key = your_secure_api_key_here改完重启:sudo systemctl restart emqx。
第五步:创建设备专用账号
EMQX Dashboard默认账号admin/admin,绝不能给ESP8266用。用curl创建设备账号:
curl -X POST http://127.0.0.1:8081/api/v5/users \ -H "Content-Type: application/json" \ -H "Authorization: ApiKey your_secure_api_key_here" \ -d '{"username":"livingroom_light","password":"strong_pass_123","tags":["device"]}'返回{"code":0,"data":{"user_id":"livingroom_light"}}即成功。这个账号只能连MQTT,不能登Dashboard,权限最小化。
第六步:配置ACL(访问控制列表)
在Dashboard的Access Control→ACL Rules里,为livingroom_light账号添加规则:
Topic: home/livingroom/light/cmd Permission: subscribeTopic: home/livingroom/light/status Permission: publish意思是:该设备只能订阅命令主题,只能发布状态主题。它想发home/kitchen/light/cmd?EMQX直接拒绝。这是安全底线,比密码还重要。
提示:ACL规则顺序很重要!EMQX按从上到下匹配,第一条匹配就停止。务必把设备专属规则放最前面,通用规则(如
#)放最后。
3.2 Android WebView App:手把手写出可商用的控制界面
HTML页面结构必须满足三个真实需求:离线可用、状态同步、操作防抖。我给出完整代码框架:
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>智能家居控制</title> <script src="mqtt.min.js"></script> <!-- MQTT.js v4.2.8 --> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; margin:0; padding:15px; } .card { background:#fff; border-radius:12px; box-shadow:0 2px 10px rgba(0,0,0,0.05); margin-bottom:15px; overflow:hidden; } .card-header { background:#4a6fa5; color:#fff; padding:12px 15px; font-weight:bold; } .btn { background:#4a6fa5; color:#fff; border:none; padding:10px 20px; border-radius:6px; margin:5px; font-size:16px; cursor:pointer; } .btn:active { opacity:0.8; } .status { padding:10px; font-size:14px; color:#666; } </style> </head> <body> <div class="card"> <div class="card-header">客厅灯光</div> <div style="padding:15px;"> <button class="btn" id="onBtn">开灯</button> <button class="btn" id="offBtn">关灯</button> <button class="btn" id="colorBtn">切换颜色</button> <div class="status" id="status">状态:未知</div> </div> </div> <script> // 1. 连接EMQX WebSocket const client = mqtt.connect('wss://your-domain.com:8084/mqtt', { clientId: 'android_' + Date.now(), username: 'android_user', password: 'android_pass_123' }); // 2. 连接成功后订阅状态主题 client.on('connect', () => { console.log('Connected to EMQX'); client.subscribe('home/livingroom/light/status', { qos: 1 }); document.getElementById('status').textContent = '状态:已连接'; }); // 3. 收到状态消息,更新UI client.on('message', (topic, payload) => { if(topic === 'home/livingroom/light/status') { const status = JSON.parse(payload.toString()); document.getElementById('status').textContent = `状态:${status.power ? '开启' : '关闭'},亮度${status.brightness}%`; } }); // 4. 按钮点击发送命令(带防抖) let lastSend = 0; function sendCommand(cmd) { const now = Date.now(); if(now - lastSend < 300) return; // 300ms内只发一次 lastSend = now; client.publish('home/livingroom/light/cmd', JSON.stringify(cmd), { qos: 1 }); console.log('Sent command:', cmd); } document.getElementById('onBtn').onclick = () => sendCommand({ power: true }); document.getElementById('offBtn').onclick = () => sendCommand({ power: false }); document.getElementById('colorBtn').onclick = () => sendCommand({ effect: 'rainbow' }); </script> </body> </html>关键细节说明:
- WebSocket地址:
wss://your-domain.com:8084/mqtt,必须是域名+HTTPS,不能用IP。免费域名推荐Freenom(.tk/.ml),或用Cloudflare Tunnel做内网穿透; - QoS等级:
qos: 1表示“至少一次”,确保指令不丢失。QoS=0可能丢包,QoS=2太重,ESP8266处理不过来; - 防抖逻辑:用户狂点“开灯”按钮,前端限制300ms内只发一次,避免EMQX堆积重复消息,也防止ESP8266被高频指令冲垮;
- 状态更新:UI显示的“亮度XX%”来自ESP8266发布的
status消息,不是前端记忆,保证状态绝对真实。
注意:Android WebView默认禁用
localStorage,所以不能用它存设备状态。所有状态必须以MQTT消息为准,这是分布式系统的铁律。
3.3 ESP8266固件开发:稳定运行三年不重启的实践技巧
Arduino代码不是写完就能用,要经过三重校验:内存校验、网络校验、指令校验。我给出生产环境验证过的模板:
main.ino
#include <ESP8266WiFi.h> #include <PubSubClient.h> #include <Adafruit_NeoPixel.h> // 1. 硬件定义 #define LED_PIN 2 #define NUM_LEDS 60 Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800); // 2. WiFi与MQTT配置(存入SPIFFS,避免硬编码) const char* ssid = "YourHomeWiFi"; const char* password = "YourWiFiPass"; const char* mqtt_server = "your-domain.com"; const int mqtt_port = 8084; // WebSocket端口 const char* mqtt_user = "livingroom_light"; const char* mqtt_pass = "strong_pass_123"; WiFiClientSecure wifiClient; PubSubClient client(mqtt_server, mqtt_port, wifiClient); // 3. 全局状态 struct LightState { bool power = false; uint8_t brightness = 100; String effect = "solid"; } state; // 4. 心跳与重连机制 unsigned long lastReconnectAttempt = 0; const long RECONNECT_INTERVAL = 5000; // 5秒重试 void setup() { Serial.begin(115200); strip.begin(); strip.show(); // 初始化灯带 connectToWiFi(); client.setServer(mqtt_server, mqtt_port); client.setCallback(callback); } void loop() { // 保持WiFi连接 if(WiFi.status() != WL_CONNECTED) { connectToWiFi(); } // 保持MQTT连接 if(!client.connected()) { reconnect(); } client.loop(); // 执行灯效(非阻塞) runLightEffect(); // 每5秒发一次状态心跳 static unsigned long lastStatus = 0; if(millis() - lastStatus > 5000) { lastStatus = millis(); publishStatus(); } } void connectToWiFi() { if(WiFi.status() == WL_CONNECTED) return; WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); while(WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi connected"); } void reconnect() { if(client.connected()) return; if(millis() - lastReconnectAttempt > RECONNECT_INTERVAL) { lastReconnectAttempt = millis(); if(client.connect("livingroom_light", mqtt_user, mqtt_pass)) { Serial.println("MQTT connected"); client.subscribe("home/livingroom/light/cmd"); } else { Serial.printf("MQTT connect failed, rc=%d\n", client.state()); } } } void callback(char* topic, byte* payload, unsigned int length) { Serial.printf("Message arrived [%s]: ", topic); String msg = ""; for(int i=0; i<length; i++) { msg += (char)payload[i]; } Serial.println(msg); // 5. 指令校验:只处理JSON,且必须含power或effect字段 if(String(topic) == "home/livingroom/light/cmd") { DynamicJsonDocument doc(512); DeserializationError error = deserializeJson(doc, msg); if(error) { Serial.println("Invalid JSON"); return; } if(doc.containsKey("power")) { state.power = doc["power"]; Serial.printf("Power set to %s\n", state.power ? "ON" : "OFF"); } if(doc.containsKey("effect")) { state.effect = doc["effect"].as<String>(); Serial.printf("Effect set to %s\n", state.effect.c_str()); } } } void publishStatus() { DynamicJsonDocument doc(256); doc["power"] = state.power; doc["brightness"] = state.brightness; doc["effect"] = state.effect; String json; serializeJson(doc, json); client.publish("home/livingroom/light/status", json.c_str(), true); } void runLightEffect() { if(!state.power) { strip.clear(); strip.show(); return; } if(state.effect == "solid") { for(int i=0; i<NUM_LEDS; i++) { strip.setPixelColor(i, strip.Color(255, 100, 0)); // 暖白 } } else if(state.effect == "rainbow") { static uint16_t j = 0; for(int i=0; i<strip.numPixels(); i++) { strip.setPixelColor(i, strip.ColorHSV((i * 256 / strip.numPixels() + j) & 65535)); } j += 256 / 30; // 速度控制 } strip.show(); }关键技巧解析:
- SPIFFS存储配置:实际项目中,
ssid和password不应硬编码,用SPIFFS.begin()读取config.json,支持App远程更新; - WiFi重连策略:
while(WiFi.status() != WL_CONNECTED)会阻塞,导致灯效卡顿。正确做法是loop()里检查状态,失败则delay(10)继续,不阻塞主循环; - MQTT心跳:
client.loop()必须在loop()里高频调用(>10Hz),否则心跳包发不出,EMQX判定设备离线; - JSON解析内存:
DynamicJsonDocument doc(512)分配512字节栈内存,足够解析10个字段的JSON。过大(如1024)会挤占WiFi缓冲区,导致连接不稳定; - 灯效非阻塞:所有
delay()都换成millis()计时,runLightEffect()函数执行时间<1ms,确保WiFi和MQTT任务及时响应。
实测心得:ESP8266连续运行超过1000小时后,
ESP.getFreeHeap()从初始120KB降到85KB,这是内存碎片导致的。解决方案是定期ESP.restart(),但必须在灯效淡出后执行,避免闪烁。我在publishStatus()里加判断:if(millis() > 86400000) ESP.restart();(24小时重启一次),用户完全无感。
4. 实操过程与核心环节实现
4.1 从零部署EMQX到上线的全流程记录
我以树莓派4B为载体,记录真实部署时间线(含所有坑):
Day 0:系统初始化(耗时25分钟)
- 烧录Raspberry Pi OS Lite 64-bit(官网最新版);
sudo raspi-config启用SSH、设置时区为Asia/Shanghai;sudo apt update && sudo apt full-upgrade -y,重启;df -h确认/boot分区有1.2GB空闲(EMQX安装包约350MB);
Day 1:EMQX安装与基础配置(耗时40分钟)
- 执行
curl https://repos.emqx.com/install-emqx-deb.sh | sudo bash,等待12分钟(树莓派编译慢); sudo apt install emqx报错:emqx : Depends: libssl1.1 but it is not installable。解决:sudo apt install libssl1.1,但Debian 11默认是libssl3。最终方案:sudo apt install -t bullseye-backports libssl1.1;- 启动
sudo systemctl start emqx,sudo journalctl -u emqx -n 50看到emqx@127.0.0.1:1883 started; - 浏览器访问
http://raspberrypi.local:18083,登录admin/public,Dashboard能打开;
Day 2:域名与HTTPS配置(耗时90分钟)
- 买Freenom域名
smart-home.tk,DNS解析指向树莓派公网IP(需路由器端口映射80/443); - 用Certbot申请Let's Encrypt证书:
sudo apt install certbot python3-certbot-nginx,但树莓派没Nginx。改用certbot certonly --standalone -d smart-home.tk,需临时停EMQX(sudo systemctl stop emqx); - 证书生成在
/etc/letsencrypt/live/smart-home.tk/,复制fullchain.pem和privkey.pem到/etc/emqx/certs/; - 修改
/etc/emqx/emqx.conf:listener.wss.external.keyfile = /etc/emqx/certs/privkey.pem listener.wss.external.certfile = /etc/emqx/certs/fullchain.pem sudo systemctl restart emqx,sudo ss -tuln | grep 8084确认端口监听;
Day 3:设备接入与压力测试(耗时60分钟)
- 用MQTTX工具连接
wss://smart-home.tk:8084/mqtt,用户名livingroom_light,密码strong_pass_123,成功; - 发送
home/livingroom/light/cmd消息:{"power":true},EMQX Dashboard的Messages页签立即显示; - 启动5个ESP8266设备(模拟不同房间),
sudo systemctl status emqx查看内存:RSS 125MB,CPU<15%,稳定; - 模拟断网:拔掉树莓派网线30秒,再插回。5台ESP8266在12秒内全部重连,无消息丢失(QoS=1保障);
关键数据记录:
- EMQX内存占用:单设备连接约8MB,5设备约125MB(含缓存);
- WebSocket连接建立时间:平均120ms(树莓派4B + 千兆路由器);
- 消息端到端延迟:Android App发指令 → EMQX → ESP8266执行,实测280~450ms,满足家居控制需求(>1秒才感知卡顿);
4.2 Android WebView App打包与发布避坑指南
Build.gradle配置(关键!)
android { compileSdk 33 defaultConfig { applicationId "com.yourcompany.smartlight" minSdk 21 // Android 5.0,覆盖98%设备 targetSdk 33 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false // 不混淆JS,否则MQTT.js报错 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') } } // 必须添加,否则WebView无法加载本地JS packagingOptions { pickFirst '**/libc++_shared.so' } }常见问题与解决:
问题1:App打开白屏
原因:index.html路径错误。正确路径是file:///android_asset/www/index.html,不是file:///android_asset/index.html;
解决:webView.loadUrl("file:///android_asset/www/index.html");问题2:MQTT连接失败,报错
WebSocket connection to 'wss://...' failed
原因:Android 9+默认禁止HTTP明文流量,但WSS是HTTPS,问题在证书。
解决:在AndroidManifest.xml的<application>标签内加:android:usesCleartextTraffic="true"并确保EMQX的SSL证书是Let's Encrypt等可信CA签发,自签名证书会被拒绝;
问题3:按钮点击无反应
原因:WebView默认禁用JavaScript。
解决:webView.getSettings().setJavaScriptEnabled(true)必须在loadUrl()之前调用;问题4:状态不更新,Console显示
Uncaught ReferenceError: mqtt is not defined
原因:mqtt.min.js未正确加载。
解决:把mqtt.min.js放在assets/www/目录,HTML中<script src="mqtt.min.js">路径必须相对,不能写<script src="./mqtt.min.js">;
发布建议:
- 不上架应用商店,用蒲公英分发平台(www.pgyer.com),上传APK后生成二维码,扫码安装;
- 版本命名规则:
v1.0.0_20231001(日期后缀),便于回溯; - 每次更新HTML,只需重新打包APK,无需用户卸载重装。
4.3 ESP8266固件烧录与现场调试实录
烧录工具链选择:
- 推荐:PlatformIO + VS Code,比Arduino IDE稳定。安装PlatformIO插件,新建Project选
Espressif 8266,框架选Arduino; - 固件大小监控:PlatformIO编译后显示
Flash: 62% (1024000 / 1638400 bytes),RAM: 38% (28000 / 73728 bytes)`,确保Flash<80%,RAM<50%;
现场调试三板斧:
串口日志分级:
#define LOG_LEVEL 2 // 0=error, 1=warn, 2=info, 3=debug #if LOG_LEVEL >= 2 Serial.printf("[INFO] WiFi connected, IP:%s\n", WiFi.localIP().toString().c_str()); #endif避免
Serial.println()刷屏,影响灯效;网络诊断命令:
在串口输入AT+PING="your-domain.com",看DNS是否通;输入AT+CIPSTART="TCP","your-domain.com",8084,看TCP是否通;MQTT状态快照:
定义命令AT+MQTTSTATE,返回:MQTT State: Connected Subscribed Topics: home/livingroom/light/cmd Last Publish: 2023-1