Android手机端远程控制ESP32引脚电平实例

一、背景介绍

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博客》

五、运行效果

以上只是点亮小灯的演示,扩展上面实例,可以实现其他一些操作。 

作者:清山博客

物联沃分享整理
物联沃-IOTWORD物联网 » Android手机端远程控制ESP32引脚电平实例

发表回复