文章目录
前言
现在物联网无处不在,最常见的就是智能家居,通过手机即可实现控制各种电气设备,如电灯、电饭煲、电冰箱等等。那么问题就来了,如何通过手机控制ESP32?🤷♂️🤷♀️。初始设想是写一个手机APP,然后再瞎捣鼓几段ESP32程序不就行了吗。想得倒是美,现实是APP选什么平台、用什么开发语言、开发工具等等,一大堆珠穆朗玛峰挡在眼前,看不到前进的方向😨。不仅如此,自己写手机APP,还需要考虑安全问题。设想一下,如果自己的APP被黑,然后被控制,那是一个怎么样的场景。
就在我夜不能寐之时,脑子突然蹦出一个想法:微信小程序不就是最好的方案吗?写起来简单、不需要考虑平台的问题(Android or IOS),用户不需要安装APP、安全又能得到保证,简直牛逼到飞起。当即决定,起床写一个微信小程序的Hello World🤣🤣。
因为我是零基础的,本文内容讲得会比较详细,适合新手。首先从原理出发,弄清楚微信小程序控制ESP32的整个流程,然后细分到微信小程序怎么编写、ESP32程序怎么编写、微信小程序如何与ESP32通信。
代码下载:
一、原理讲解
1、整体控制原理
系统初始化后,微信小程序、ESP32都与MQTT服务器连接。用户通过微信小程序发送控制信号,MQTT服务器接收到控制信号后,转发给ESP32,ESP32解析控制信号并实施。
2、MQTT协议
MQTT是一种协议,百度是这么说的:MQTT(消息队列遥测传输)是ISO 标准(ISO/IEC PRF 20922)下基于发布/订阅范式的消息协议。它工作在 TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议,为此,它需要一个消息中间件 。 MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。
MQTT总结如下:
- 分为客户端、服务器
- 每个客户端可以接收数据(订阅)、发送数据(发布)
- 每个消息都由主题(
topic
)+消息内容(data
)组成
MQTT服务器可以同时连接多个客户端,这里的客户端包括微信小程序、ESP32开发板运行的程序等等。每个客户端都可以订阅和发布主题,主题下包含消息。当其中一个客户端发布一个名为topic_1
的主题时,其他订阅topic_1
的客户端都会接收到该主题下的消息,也就实现了通信。
为什么 MQTT 是适用于物联网的最佳协议?
据 IoT Analytics 最新发布的《2022 年春季物联网状况》研究报告显示,到 2022 年,物联网市场预计将增长 18%,达到 144 亿活跃连接。
在如此大规模的物联网需求下,海量的设备接入和设备管理对网络带宽、通信协议以及平台服务架构都带来了巨大的挑战。对于物联网协议来说,必须针对性地解决物联网设备通信的几个关键问题:网络环境复杂而不可靠、内存和闪存容量小、处理器能力有限。
MQTT 协议正是为了应对以上问题而创建,经过多年的发展凭借其轻量高效、可靠的消息传递、海量连接支持、安全的双向通信等优点已成为物联网行业的首选协议。
二、MQTT服务器
我们可以将MQTT当做一个软件,而MQTT服务器则是安装该软件的电脑。通过MQTT服务器,我们可以实现非常多的设备同时接入互联网,并实现非常方便的管理。
1、免费测试服务器(推荐)
EMQ是一个开源物联网数据基础设施软件供应商,它提供了在线免费的MQTT测试服务器。初学者可以借此机会学习物联网,实现在公网下控制自己ESP32。网址:免费的公共 MQTT 服务器 | EMQ (emqx.com)
开发人员无需用户名和密码即可连接该服务器,非常方便。
2、在自己电脑搭建
如果不想使用上述免费的服务器,也可以在自己电脑搭建一个,我们可以使用EMQ提供的MQTT服务器软件,下载之后即可直接运行。网址:EMQX: 大规模分布式物联网 MQTT 消息服务器
下载
选择适合自己电脑的版本,这里选择Windows为例
安装提示,下载解压,进入bin文件夹
进入bin文件夹
输入cmd后回车,即可快速进入在此文件夹下打开控制台窗口
输入 eqmx start 启动MQTT服务器
在浏览器中输入:http://localhost:18083/,打开MQTT服务器控制后台
3、购买大公司的服务
没钱测试🤣🤣🤣,后续买了再补充。。。
三、ESP32
0、入门资料
对于初学者者,一门好的入门资料是必不可少的。我学习ESP32的入门资料是王老师的视频:Python+ESP32 快速上手(持续更新中) wifi 蓝牙 智能控制 单片机_哔哩哔哩_bilibili。实操的感觉非常棒。然后就是micropython的官方文档:MicroPython 文档— MicroPython中文 1.17 文档
1、配网
配网流程
ESP32配网测试代码
import network
import socket
import ure
import time
NETWORK_PROFILES = 'wifi.dat'
wlan_ap = network.WLAN(network.AP_IF)
wlan_sta = network.WLAN(network.STA_IF)
server_socket = None
def send_header(conn, status_code=200, content_length=None ):
conn.sendall("HTTP/1.0 {} OK\r\n".format(status_code))
conn.sendall("Content-Type: text/html\r\n")
if content_length is not None:
conn.sendall("Content-Length: {}\r\n".format(content_length))
conn.sendall("\r\n")
def send_response(conn, payload, status_code=200):
content_length = len(payload)
send_header(conn, status_code, content_length)
if content_length > 0:
conn.sendall(payload)
conn.close()
def config_page():
return b"""<html>
<head>
<title>MYESP8266 AP Test</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Wifi 配网</h1>
<form action="configure" method="post">
<div>
<label>SSID</label>
<input type="text" name="ssid">
</div>
<div>
<label>PASSWORD</label>
<input type="password" name="password">
</div>
<input type="submit" value="连接">
<form>
</body>
</html>"""
def wifi_conf_page(ssid, passwd):
return b"""<html>
<head>
<title>Wifi Conf Info</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Post data:</h1>
<p>SSID: %s</p>
<p>PASSWD: %s</p>
<a href="/">Return Configure Page</a>
</body>
</html>""" % (ssid, passwd)
def connect_sucess(new_ip):
return b"""<html>
<head>
<title>Connect Sucess!</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p>Wifi Connect Sucess</p>
<p>IP Address: %s</p>
<a href="http://192.168.4.1">Home</a>
<a href="/disconnect">Disconnect</a>
</body>
</html>""" % (new_ip)
def connect_fail(new_ip):
return b"""<html>
<head>
<title>Connect Fail!</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p>Wifi Connect Fail</p>
<p>IP Address: %s</p>
<a href="http://192.168.4.1">Home</a>
<a href="/disconnect">Disconnect</a>
</body>
</html>""" % (new_ip)
def get_wifi_conf(request):
match = ure.search("ssid=([^&]*)&password=(.*)", request)
if match is None:
return False
try:
ssid = match.group(1).decode("utf-8").replace("%3F", "?").replace("%21", "!")
password = match.group(2).decode("utf-8").replace("%3F", "?").replace("%21", "!")
except Exception:
ssid = match.group(1).replace("%3F", "?").replace("%21", "!")
password = match.group(2).replace("%3F", "?").replace("%21", "!")
if len(ssid) == 0:
return False
return (ssid, password)
def handle_wifi_configure(ssid, password):
if do_connect(ssid, password):
# try:
# profiles = read_profiles()
# except OSError:
# profiles = {}
# profiles[ssid] = password
# write_profiles(profiles)
#
# time.sleep(5)
#
new_ip = wlan_sta.ifconfig()[0]
return new_ip
else:
print('connect fail')
return False
def check_wlan_connected():
if wlan_sta.isconnected():
return True
else:
return False
def do_connect(ssid, password):
connected = False
try:
wlan_sta.active(True)
if wlan_sta.isconnected():
return None
print('Connect to %s' % ssid)
wlan_sta.connect(ssid, password)
for retry in range(100):
connected = wlan_sta.isconnected()
if connected:
break
time.sleep(0.1)
print('.', end='')
if connected:
print('\nConnected : ', wlan_sta.ifconfig())
else:
print('\nFailed. Not Connected to: ' + ssid)
except:
pass
return connected
def read_profiles():
with open(NETWORK_PROFILES) as f:
lines = f.readlines()
profiles = {}
for line in lines:
ssid, password = line.strip("\n").split(";")
profiles[ssid] = password
return profiles
def write_profiles(profiles):
lines = []
for ssid, password in profiles.items():
lines.append("%s;%s\n" % (ssid, password))
with open(NETWORK_PROFILES, "w") as f:
f.write(''.join(lines))
def stop():
global server_socket
if server_socket:
server_socket.close()
server_socket = None
def startAP():
global server_socket
stop()
wlan_ap.active(True)
wlan_ap.config(essid='ESP32-AP',authmode=0)
server_socket = socket.socket()
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 80))
server_socket.listen(3)
while not wlan_sta.isconnected():
conn, addr = server_socket.accept()
print('Connection: %s ' % str(addr))
try:
conn.settimeout(3)
request = b""
try:
while "\r\n\r\n" not in request:
request += conn.recv(512)
except OSError:
pass
# url process
try:
url = ure.search("(?:GET|POST) /(.*?)(?:\\?.*?)? HTTP", request).group(1).decode("utf-8").rstrip("/")
except Exception:
pass
# url = ure.search("(?:GET|POST) /(.*?)(?:\\?.*?)? HTTP", request).group(1).rstrip("/")
print("URL is {}".format(url))
if url == "":
response = config_page()
send_response(conn, response)
elif url == "configure":
ret = get_wifi_conf(request)
ret = handle_wifi_configure(ret[0], ret[1])
if ret is not None:
if ret is not False:
response = connect_sucess(ret)
send_response(conn, response)
print('connect sucess')
else:
response = connect_fail(ret)
send_response(conn, response)
print('connect fail')
elif url == "disconnect":
wlan_sta.disconnect()
response = config_page()
send_response(conn, response)
finally:
conn.close()
#发送成功连接的页面才退出AP
# wlan_ap.active(False)
# print('ap exit')
def home():
global server_socket
stop()
wlan_sta.active(True)
ip_addr = wlan_sta.ifconfig()[0]
print('wifi connected')
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 80))
server_socket.listen(3)
while check_wlan_connected():
conn, addr = server_socket.accept()
try:
conn.settimeout(3)
request = b""
try:
while "\r\n\r\n" not in request:
request += conn.recv(512)
except OSError:
pass
# url process
try:
url = ure.search("(?:GET|POST) /(.*?)(?:\\?.*?)? HTTP", request).group(1).decode("utf-8").rstrip("/")
except Exception:
url = ure.search("(?:GET|POST) /(.*?)(?:\\?.*?)? HTTP", request).group(1).rstrip("/")
if url == "":
response = connect_sucess(ip_addr)
send_response(conn, response)
elif url == "configure":
ret = get_wifi_conf(request)
response = connect_sucess(ret[0])
send_response(conn, response)
elif url == "disconnect":
wlan_sta.disconnect()
response = config_page()
send_response(conn, response)
finally:
conn.close()
# wlan_sta.active(False)
# print('sta exit')
def main():
print("runing main")
while True:
if not check_wlan_connected():
startAP()
else:
home()
#这里应该放MQTT客户端代码
if __name__=="__main__":
#浏览器输入192.168.4.1后,有时须断开流量开关才可进入。
#点击网页的“连接”,是将该网页返回,手机网页将数据发送给esp32,esp32将接收的数据解析。
main()
👀在Thonny中创建文件wifi_config.py并保存到ESP32,粘贴以上代码后运行。
👀运行
👀手机连接ESP32创建的WIFI:ESP32-AP
👀然后在手机浏览器中输入192.168.4.1
,这时会弹出WiFi配置ESP32WiFi网页,然后输入家里的WiFi,
👀连接成功浏览器会弹出以下网页
👀否则
2、MQTT连接
umqttsimple.py是MQTT客户端代码,由于ESP32视作客户端,因此需要调用此代码
import usocket as socket
import ustruct as struct
from ubinascii import hexlify
class MQTTException(Exception):
pass
class MQTTClient:
def __init__(
self,
client_id,
server,
port=0,
user=None,
password=None, #user、password既可以不设置,也可以让mqtt服务器生成。
keepalive=0,
ssl=False,
ssl_params={},
):
if port == 0:
port = 8883 if ssl else 1883
self.client_id = client_id
self.sock = None
self.server = server
self.port = port
self.ssl = ssl
self.ssl_params = ssl_params
self.pid = 0
self.cb = None
self.user = user
self.pswd = password
self.keepalive = keepalive
self.lw_topic = None
self.lw_msg = None
self.lw_qos = 0
self.lw_retain = False
def _send_str(self, s):
self.sock.write(struct.pack("!H", len(s)))
self.sock.write(s)
def _recv_len(self):
n = 0
sh = 0
while 1:
b = self.sock.read(1)[0]
n |= (b & 0x7F) << sh
if not b & 0x80:
return n
sh += 7
def set_callback(self, f):
self.cb = f
def set_last_will(self, topic, msg, retain=False, qos=0):
assert 0 <= qos <= 2
assert topic
self.lw_topic = topic
self.lw_msg = msg
self.lw_qos = qos
self.lw_retain = retain
def connect(self, clean_session=True):
self.sock = socket.socket()
addr = socket.getaddrinfo(self.server, self.port)[0][-1]
print("MQTT server addr",addr)
self.sock.connect(addr)
if self.ssl:
import ussl
self.sock = ussl.wrap_socket(self.sock, **self.ssl_params)
premsg = bytearray(b"\x10\0\0\0\0\0")
msg = bytearray(b"\x04MQTT\x04\x02\0\0")
sz = 10 + 2 + len(self.client_id)
msg[6] = clean_session << 1
if self.user is not None:
sz += 2 + len(self.user) + 2 + len(self.pswd)
msg[6] |= 0xC0
if self.keepalive:
assert self.keepalive < 65536
msg[7] |= self.keepalive >> 8
msg[8] |= self.keepalive & 0x00FF
if self.lw_topic:
sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
msg[6] |= self.lw_retain << 5
i = 1
while sz > 0x7F:
premsg[i] = (sz & 0x7F) | 0x80
sz >>= 7
i += 1
premsg[i] = sz
self.sock.write(premsg, i + 2)
self.sock.write(msg)
# print(hex(len(msg)), hexlify(msg, ":"))
self._send_str(self.client_id)
if self.lw_topic:
self._send_str(self.lw_topic)
self._send_str(self.lw_msg)
if self.user is not None:
self._send_str(self.user)
self._send_str(self.pswd)
resp = self.sock.read(4)
assert resp[0] == 0x20 and resp[1] == 0x02
if resp[3] != 0:
raise MQTTException(resp[3])
return resp[2] & 1
def disconnect(self):
self.sock.write(b"\xe0\0")
self.sock.close()
def ping(self):
self.sock.write(b"\xc0\0")
def publish(self, topic, msg, retain=False, qos=0):
pkt = bytearray(b"\x30\0\0\0")
pkt[0] |= qos << 1 | retain
sz = 2 + len(topic) + len(msg)
if qos > 0:
sz += 2
assert sz < 2097152
i = 1
while sz > 0x7F:
pkt[i] = (sz & 0x7F) | 0x80
sz >>= 7
i += 1
pkt[i] = sz
# print(hex(len(pkt)), hexlify(pkt, ":"))
self.sock.write(pkt, i + 1)
self._send_str(topic)
if qos > 0:
self.pid += 1
pid = self.pid
struct.pack_into("!H", pkt, 0, pid)
self.sock.write(pkt, 2)
self.sock.write(msg)
if qos == 1:
while 1:
op = self.wait_msg()
if op == 0x40:
sz = self.sock.read(1)
assert sz == b"\x02"
rcv_pid = self.sock.read(2)
rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
if pid == rcv_pid:
return
elif qos == 2:
assert 0
def subscribe(self, topic, qos=0):
assert self.cb is not None, "Subscribe callback is not set"
pkt = bytearray(b"\x82\0\0\0")
self.pid += 1
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
# print(hex(len(pkt)), hexlify(pkt, ":"))
self.sock.write(pkt)
self._send_str(topic)
self.sock.write(qos.to_bytes(1, "little"))
while 1:
op = self.wait_msg()
if op == 0x90:
resp = self.sock.read(4)
# print(resp)
assert resp[1] == pkt[2] and resp[2] == pkt[3]
if resp[3] == 0x80:
raise MQTTException(resp[3])
return
# Wait for a single incoming MQTT message and process it.
# Subscribed messages are delivered to a callback previously
# set by .set_callback() method. Other (internal) MQTT
# messages processed internally.
def wait_msg(self):
res = self.sock.read(1)
self.sock.setblocking(True)
if res is None:
return None
if res == b"":
raise OSError(-1)
if res == b"\xd0": # PINGRESP
sz = self.sock.read(1)[0]
assert sz == 0
return None
op = res[0]
if op & 0xF0 != 0x30:
return op
sz = self._recv_len()
topic_len = self.sock.read(2)
topic_len = (topic_len[0] << 8) | topic_len[1]
topic = self.sock.read(topic_len)
sz -= topic_len + 2
if op & 6:
pid = self.sock.read(2)
pid = pid[0] << 8 | pid[1]
sz -= 2
msg = self.sock.read(sz)
self.cb(topic, msg)
if op & 6 == 2:
pkt = bytearray(b"\x40\x02\0\0")
struct.pack_into("!H", pkt, 2, pid)
self.sock.write(pkt)
elif op & 6 == 4:
assert 0
# Checks whether a pending message from server is available.
# If not, returns immediately with None. Otherwise, does
# the same processing as wait_msg.
def check_msg(self):
self.sock.setblocking(False)
return self.wait_msg()
mqtt_client.py是ESP32连接MQTT服务器的测试代码
import time
import network
from umqttsimple import MQTTClient
from machine import Pin
def do_connect():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print('connecting to network...')
wlan.connect('你的WiFi名称', '你的WiFi密码')
i = 1
while not wlan.isconnected():
print("正在链接...{}".format(i))
i += 1
time.sleep(1)
print('network config:', wlan.ifconfig())
def sub_cb(topic, msg): # 回调函数,收到服务器消息后会调用这个函数
print(topic, msg)
if topic.decode("utf-8")=="ledctl" and msg.decode("utf-8")=="on":
led_pin.value(1)
elif topic.decode("utf-8")=="ledctl" and msg.decode("utf-8")=="off":
led_pin.value(0)
if __name__=="__main__":
# 1. 联网
do_connect()
# 2. 创建mqtt客户端
mqtt_client = MQTTClient(client_id="umqtt_client", server="broker-cn.emqx.io",port=1883) # 建立一个MQTT客户端
mqtt_client.set_callback(sub_cb) # 设置回调函数
mqtt_client.connect() # 建立与MQTT服务器的连接
mqtt_client.subscribe(b"ledctl") # 监控ledctl这个通道,接收控制命令
led_pin = Pin(2, Pin.OUT)
while True:
mqtt_client.check_msg()
time.sleep(1)
👀运行mqtt_client.py后,Thonny会打印MQTT服务器信息,说明连接成功。这里连接的是EMQ的MQTT在线测试服务器
怎么测试呢?我们使用EMQ提供的桌面端MQTT客户端MQTT X:跨平台 MQTT 5.0 桌面客户端工具,下载默认安装即可。打开MQTT客户端工具界面如下
在客户端中新建连接,填写MQTT服务器信息,并连接
我们发送一个主题为:ledctl,信息为:hello ESP32!
查看Thonny打印ESP32收到的信息
四、微信小程序
好了,前面已经知道如何通过电脑给ESP32发送信息了,接下来继续如何通过微信小程序给ESP32发送信息。
0、入门资料
入门资料感觉还是先看视频,然后辅助官网文档查询资料,然后找一个你想做的项目类似版本实战。我推荐的资料有以下几个:
1、微信小程序结构
一般看一个程序,都是先看入口程序在哪。小程序的入口程序是。然后运行过程,如果是APP,则叫生命周期,小程序的生命周期看这个小程序运行机制 | 微信开放文档 (qq.com)和生命周期 | 微信开放文档 (qq.com)。
小程序所用的编程语言是网页老三样js、html5和css,htm5负责把内容填上网页、css负责把内容排版好,js则是执行如按钮点击后的逻辑操作,三者相互配合完成前端交互。总体看看小程序的结构(目录结构 | 微信开放文档 (qq.com))。再看看小程序框架WXML | 微信开放文档 (qq.com)。
2、微信小程序开源项目
好了,到了最关键的:码代码,一般看视频码一码代码找找手感,然后找个开源项目改改练练手。本文找了个能直接连接MQTT服务器的小程序(MQTT-Client-Examples/mqtt-client-wechat-miniprogram at master · emqx/MQTT-Client-Examples (github.com))。
我们在真机上测试,手机和ESP32同时订阅主题:ledctl,然后手机端发送如图消息
可以看到Thonny打印ESP32收到的信息,让ESP32对不同的信息执行不同的动作,那么就可以实现小程序控制ESP32❤❤❤
五、系统总体运行测试
在Thonny中打开项目文件夹,右键下载到ESP32
ESP32运行脚本
然后打开微信小程序项目,并真机调试
效果
总结
本文以手机控制ESP32需求出发,使用微信小程序作为移动端,应用MQTT协议通信,实现微信小程序控制ESP32。该项目可应用在许多应用场景,诸如智能家居、数据监控等等场景,具有很强的使用意义。本项目实现了微信小程序远程控制ESP32的最基本流程搭建,基于此可实现快速的项目开发。