一、背景介绍
ESP32是一款高度集成的低功耗系统级芯片,它结合了双核处理器、无线通信、低功耗特性和丰富的外设,适用于各种物联网(IoT)应用,如果与Android结合,在手机端远程控制ESP32引脚高低电平输出,也是一件挺有趣的事情。
二、工作原理
我们这里使用MQTT协议来实现手机App与ESP32之间的通信。MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)协议是一种基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,它工作在TCP/IP协议族上,特别适用于计算能力有限且工作在低带宽、不可靠的网络的远程传感器和控制设备通讯。大概的原理图如下:
三、准备工作
要实现上述功能,我们需要准备以下东西:
1.可用于开发的Android手机(本文以小米手机为例)。
2.MQTT服务器:可以使用免费的公共 MQTT 服务器 | EMQ,也可以参考《MQTT服务器-安装篇(阿里云主机)-CSDN博客》自己搭建MQTT服务器。
3.配置好开发环境的ESP32:可以参考《01_ESP32 MicroPython开发环境搭建-CSDN博客》
四、实现步骤
我们假设已经完成了“准备工作”中的几项,开始实现功能,先来说下思路:
整个过程包括2个步骤:
一是手机端要能获取到ESP32当前引脚的状态。
二是手机端给ESP32发送指令,设置引脚状态。
根据消息流方向,我们这里使用2个MQTT主题(名称可以自定义):
esp32_down:用于手机端向ESP32发送指令,手机端向此主题发送消息,ESP32订阅该主题。
esp32_up:用于ESP32向手机端上报状态,ESP32向此主题发送消息,手机端订阅该主题。
我们以点亮ESP32上蓝色的小灯为例,演示操作。
开始程序编写
1.ESP32端
import time
import network
import utime
from umqtt.robust import MQTTClient
from machine import Pin
#GPIO引脚2,用于开灯或关灯
pin2 = Pin(2, Pin.OUT)
# WiFi 连接信息
wifi_ssid = '******' # 请替换为自己的WiFi
wifi_password = '******' # 请替换为自己的WiFi
# MQTT 服务器信息
mqtt_server = '******' # 请替换为自己的配置
mqtt_port = 1883
mqtt_user = '******' # 请替换为自己的配置
mqtt_password = '******'# 请替换为自己的配置
mqtt_client = 'esp32' # 客户端ID
mqtt_topic_down = 'esp32_down' # 给ESP32发送指令的主题
mqtt_topic_up = 'esp32_up' # ESP32上报状态的主题
mqtt_keepalive = 62 # 保持活动的时间间隔(秒)
# 当前时间
def currentTime():
# 获取当前时间元组
current_time = utime.localtime()
# 解构时间元组
year, month, day, hour, minute, second, _, _ = current_time
# 使用字符串格式化功能生成日期时间字符串
formatted_time = "{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d} ".format(year, month, day, hour, minute, second)
return formatted_time
# 连接 WiFi
def connect_wifi():
wifi = network.WLAN(network.STA_IF)
#若已连接过Wifi,切换新的Wifi时,需要重置下网络,才能连接成功
if wifi.isconnected():
# 断开当前连接
wifi.disconnect()
# 关闭接口
wifi.active(False)
time.sleep(1) # 休眠1秒
wifi.active(True)
print(currentTime()+"connecting to wifi: {0}".format(wifi_ssid))
wifi.connect(wifi_ssid, wifi_password)
while not wifi.isconnected():
time.sleep(1)
print(currentTime()+"connected to wifi: {0}".format(wifi_ssid))
print(currentTime()+'network config:', wifi.ifconfig())
# 定义消息回调函数
def sub_cb(topic, msg):
action=msg.decode('utf-8')
print(currentTime()+"recv: topic("+topic.decode('utf-8')+")->msg:" +action)
if(action=="on"):
lightON()
elif (action=="off"):
lightOFF()
elif (action=="status"):
uploadStatus()
#亮灯(也可以是其他操作,此处仅做演示)
def lightON():
pin2.value(1)
client.publish(mqtt_topic_up, str(1))#动态取引脚电平不准确,直接写固定值
#关灯(也可以是其他操作,此处仅做演示)
def lightOFF():
pin2.value(0)
client.publish(mqtt_topic_up, str(0))#动态取引脚电平不准确,直接写固定值
#上报当前状态
def uploadStatus():
client.publish(mqtt_topic_up, str(pin2.value()))
print(currentTime()+"send: topic("+mqtt_topic_up+')->msg:' +str(pin2.value()))
#【主程序】
connect_wifi()
mqtt_client = "esp32" + str(time.ticks_ms()) # 确保每次连接都有唯一的client_id
client = MQTTClient(mqtt_client, mqtt_server, mqtt_port,mqtt_user, mqtt_password, keepalive=mqtt_keepalive)
#设置订阅回调
client.set_callback(sub_cb)
try:
#连接到MQTT服务器
client.connect()
print(currentTime()+"ESP32已连接到MQTT服务器"+mqtt_server)
# 订阅主题
client.subscribe(mqtt_topic_down)
print(currentTime()+"ESP32已开启消息订阅,请在EMQ客户端发送消息...")
except OSError as e:
print(currentTime()+'Connection or subscription failed: %r' % e)
# 持续接收消息
count=0
while True:
count=count+1
#每次轮询间隔0.3秒,轮询200次就是60秒,在第200次时发送心跳包
if(count==199):
#发送心跳包,上报当前状态
curtime=currentTime()
uploadStatus()
print(currentTime()+"heart beat...")
count=0
client.check_msg()
time.sleep(0.3)
# 断开与MQTT代理的连接
client.disconnect()
2.MQTT服务器端
参考《MQTT服务器-安装篇(阿里云主机)-CSDN博客》
3.Android手机端
import android.annotation.SuppressLint;
import android.graphics.Color;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.ActionBar;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
public class Esp32Activity extends BaseActivity {
protected TextView txtLightStatus;
protected Button btnTest;
protected String ac = "off";
String context = "tcp://******:1883";
String clientId = "******";
String username = "******";
String password = "******";
String topic_down = "esp32_down";
String topic_up = "esp32_up";
MqttClient client;
MqttConnectOptions options;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_esp32);
//不显示标题
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
SetStatusBarTransparent();//状态栏透明
SetStatusBarTextColor(true);//状态栏文字变黑
txtLightStatus = findViewById(R.id.txtLightStatus);
btnTest = findViewById(R.id.btnTest);
new Thread(() -> {
try {
client = new MqttClient(context, clientId, new MemoryPersistence());
options = new MqttConnectOptions();
options.setCleanSession(true);
options.setUserName(username);
options.setPassword(password.toCharArray());
//订阅状态上报消息
MqttCallback callback = new MqttCallback() {
public void connectionLost(Throwable cause) {
// 连接丢失后,一般在这里面进行重连
System.out.println("Connection lost!");
}
public void messageArrived(String topic, MqttMessage message) throws Exception {
Message m = new Message();
m.what = 3;
m.obj = new String(message.getPayload());
handler.sendMessage(m);
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
System.out.println("Delivery complete");
}
};
client.setCallback(callback);
client.connect(options);
int[] QoS = {0};
client.subscribe(new String[]{topic_up}, QoS);
//发送查询状态消息
String action = "status";
MqttMessage message = new MqttMessage(action.getBytes());
message.setQos(0);
client.publish(topic_down, message);
} catch (MqttException e) {
Message message = new Message();
message.what = 2;
message.obj = e.getMessage();
handler.sendMessage(message);
}
}).start();
btnTest.setOnClickListener(view -> {
new Thread(() -> {
try {
//发送消息
String action = ac;
MqttMessage mqttMessage = new MqttMessage(action.getBytes());
mqttMessage.setQos(0);
client.publish(topic_down, mqttMessage);
Message message = new Message();
message.what = 1;
message.obj = action;
handler.sendMessage(message);
} catch (MqttException e) {
Message message = new Message();
message.what = 2;
message.obj = e.getMessage();
handler.sendMessage(message);
}
}).start();
});
}
public Handler handler = new Handler() {
@SuppressLint("HandlerLeak")
@Override
public void handleMessage(Message msg) {
String text = msg.obj.toString();
if (text.equals("on")) {
playVotice();
btnTest.setText("关灯");
}
if (text.equals("off")) {
playVotice();
btnTest.setText("开灯");
}
switch (msg.what) {
case 1:
Toast.makeText(Esp32Activity.this, "发送命令:" + ac + " 成功", Toast.LENGTH_SHORT).show();
break;
case 2:
Toast.makeText(Esp32Activity.this, "抱歉,发送异常:" + text, Toast.LENGTH_SHORT).show();
break;
case 3:
txtLightStatus.setText("当前状态:" + (text.equals("1") ? "点亮" : "熄灭"));
if (text.equals("1")) {
ac = "off";
btnTest.setBackgroundColor(Color.GREEN);
btnTest.setText("关灯");
}
if (text.equals("0")) {
ac = "on";
btnTest.setBackgroundColor(Color.GRAY);
btnTest.setText("开灯");
}
break;
}
}
};
public void playVotice() {
Uri notificationUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
Ringtone ringtone = RingtoneManager.getRingtone(Esp32Activity.this, notificationUri);
ringtone.play();
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".Esp32Activity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/menu_esp32"
android:textAlignment="center"
android:textStyle="bold"
android:textSize="24dp">
</TextView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="vertical"
android:paddingLeft="15dp"
android:paddingRight="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal">
<Button
android:id="@+id/btnTest"
android:layout_width="250dp"
android:layout_height="100dp"
android:layout_marginTop="10dp"
android:text="开灯"
android:textAlignment="center" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal">
<TextView
android:id="@+id/txtLightStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="当前状态"
android:textSize="22dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
此处只列出前台和后台代码,前台代码及配置请参考《Android 发送MQTT消息-CSDN博客》
五、运行效果
以上只是点亮小灯的演示,扩展上面实例,可以实现其他一些操作。