安卓APP控制ESP32

目录

初衷

技术栈

安卓端

ESP32端


初衷

        本人所学专业为电力电子专业,想做关于负载的连续变化实验,变化频率一定,首先想到的是电子负载,但是实验室中现有的电子负载无法应用与所研究的小功率变换器,于是想通过单片机来控制继电器进行负载切换。

        最简单的方法是PC通过串口连接单片机,单片机控制继电器吸合,进行负载的断开与连接。

        但是这一点都不好玩,太枯燥,不符合创客精神,是时候尝试各种开发的结合!

        控制必须远程,远程必须联网,联网显示QR,APP扫码连接。(还有另外一种方式SmartConfig,跟AirKiss技术类似,通过广播的方式,将WiFi信息间接传递给ESP32单片机)

       对于单片机的选择,具备联网功能的ESP32 WROOM,价格美丽,资源足够。

        安卓APP开发有多种方式,最简单的是使用web相关技术开发,uniapp,webapp等,对于页面质量的提升极其明显,但是感觉不够接近底层,对于底层逻辑的开发体验不如原生,因此使用Android Studio从而通过java进行app开发。

        成果展示:

        手机APP:

技术栈

        1.C控制单片机配置网络(手机作为服务器,ESP32为客户端),通过二维码显示设备ip信息(设备:ESP32 WROOM,TFT屏幕,5V继电器)

        2.安卓原生程序开发,扫码连接设备,从而控制单片机,同时显示单片机状态。(Android Studio)

安卓端

    为实现扫码功能,使用了华为的统一扫码服务HMS scan kiticon-default.png?t=N7T8https://developer.huawei.com/consumer/cn/hms/huawei-scankit

     (挺好用的,简单配置一下即可,添加相关依赖)

(1)首先是Manifest配置,添加相机权限,网络权限等

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!--相机权限-->
    <uses-permission android:name="android.permission.CAMERA" />
    <!--读文件权限-->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <!--使用特性-->
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission>
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.ESP32IOT"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 (2)添加gradle依赖包

//build.gradle

plugins {
    id 'com.android.application'
}

android {
    namespace 'com.example.esp32_iot'
    compileSdk 33

    defaultConfig {
        applicationId "com.example.esp32_iot"
        minSdk 29
        targetSdk 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    implementation "com.huawei.hms:scanplus:1.1.3.301"
}

(3)界面设计

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:background="@drawable/app_esp32"
    >


    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="2">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="未连接"
            android:id="@+id/tv"
            android:textSize="50sp"
            android:textColor="#dcdcdc"
            android:textColorHint="#dcdcdc"
            android:layout_gravity="center"
            android:gravity="center"
            ></TextView>
    </FrameLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/RL"
        android:layout_weight="1">
        <ImageView
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="@drawable/baseline_qr_code_scanner_24"
            android:layout_centerHorizontal="true"
            android:onClick="loadScanKitBtnClick"
            android:layout_marginLeft="20dp"
            />

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:hint="服务器地址"
            android:textSize="20sp"
            android:id="@+id/IPet"
            android:textColorHint="#dcdcdc"

            android:layout_alignParentLeft="true"
            ></EditText>
        <EditText
            android:layout_width="wrap_content"
            android:layout_height="50sp"
            android:hint="端口地址"
            android:id="@+id/port"
            android:textSize="20sp"
            android:textColorHint="#dcdcdc"
            android:layout_alignParentRight="true"
            ></EditText>
    </RelativeLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="4"
        android:orientation="vertical"
        android:gravity="center">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_weight="4"
            android:gravity="bottom"
            >
            <Button
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:text="连接"
                android:textSize="40sp"
                android:id="@+id/conn"
                android:layout_weight="1"
                android:layout_marginHorizontal="10dp"
                android:background="@drawable/btn"
                ></Button>
            <Button
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:textSize="40sp"
                android:text="断开"
                android:id="@+id/disconn"
                android:layout_weight="1"
                android:layout_marginHorizontal="10dp"
                android:background="@drawable/btn"
                ></Button>
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="4"
            android:gravity="top"
            android:layout_marginTop="20dp"
            android:paddingBottom="50dp"
            >
            <Button
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:text="ON"
                android:textSize="40sp"
                android:id="@+id/on"
                android:layout_weight="1"
                android:layout_marginHorizontal="10dp"
                android:background="@drawable/btn"
                ></Button>
            <Button
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:text="OFF"
                android:textSize="40sp"
                android:id="@+id/off"
                android:layout_weight="1"
                android:layout_marginHorizontal="10dp"
                android:background="@drawable/btn"
                ></Button>
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="700"
            ></LinearLayout>

    </LinearLayout>


</androidx.appcompat.widget.LinearLayoutCompat>

 (4)逻辑实现

//MainActicity.java

package com.example.esp32_iot;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.StrictMode;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.huawei.hms.hmsscankit.ScanUtil;
import com.huawei.hms.ml.scan.HmsScan;
import com.huawei.hms.ml.scan.HmsScanAnalyzerOptions;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    private TextView tv;
    private EditText IPet,port;
    private Handler myhandler;
    private Socket socket;
    private String str = "";
    boolean running = false;
    private Button conn,disconn,on,off;
    private StartThread st;
    private ReceiveThread rt;

    public static final int CAMERA_REQ_CODE = 111;
    public static final int DECODE = 1;
    private static final int REQUEST_CODE_SCAN_ONE = 0X01;
    String[] permissions = new String[]{Manifest.permission.READ_PHONE_STATE};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv = findViewById(R.id.tv);

        IPet = findViewById(R.id.IPet);
        port = findViewById(R.id.port);

        conn = (Button) findViewById(R.id.conn);
        disconn = (Button) findViewById(R.id.disconn);
        on = (Button) findViewById(R.id.on);
        off = findViewById(R.id.off);

        setButtonOnStartState(true);//设置按钮状态为可点击-也就是可以开始连接

        conn.setOnClickListener(this);
        disconn.setOnClickListener(this);
        on.setOnClickListener(this);
        off.setOnClickListener(this);

        myhandler = new MyHandler();

    }

    public void loadScanKitBtnClick(View view) {
        requestPermission(CAMERA_REQ_CODE, DECODE);
    }

    //编辑请求权限
    private void requestPermission(int requestCode, int mode) {
        ActivityCompat.requestPermissions(
                this,
                new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE},
                requestCode);
    }

    //权限申请返回
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (permissions == null || grantResults == null) {
            return;
        }

        if (grantResults.length < 2 || grantResults[0] != PackageManager.PERMISSION_GRANTED || grantResults[1] != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        if (requestCode == CAMERA_REQ_CODE) {
            ScanUtil.startScan(this, REQUEST_CODE_SCAN_ONE, new HmsScanAnalyzerOptions.Creator().create());
        }
    }

    //Activity回调
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode != RESULT_OK || data == null) {
            return;
        }
        if (requestCode == REQUEST_CODE_SCAN_ONE) {
            HmsScan obj = data.getParcelableExtra(ScanUtil.RESULT);
            if (obj != null) {
                Toast.makeText(this,obj.originalValue,Toast.LENGTH_SHORT).show();
                IPet.setText(obj.originalValue);
            }
        }
    }


    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.conn:
                //此时按下开始连接按键新建线程
                st = new StartThread();
                st.start();
                setButtonOnStartState(false);
                break;
            case R.id.on:
                //发送请求数据
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            OutputStream os = null;
                            os = socket.getOutputStream();//得到socket的输出流
                            //输出EditText里面的数据,数据最后加上换行符才可以让服务器端的readline()停止阻塞
                            os.write(("123\n").getBytes("utf-8"));
                        }catch (IOException e){
                            e.printStackTrace();
                        }
                    }
                }).start();
                break;
            case R.id.off:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            OutputStream os = null;
                            os = socket.getOutputStream();//得到socket的输出流
                            //输出EditText里面的数据,数据最后加上换行符才可以让服务器端的readline()停止阻塞
                            os.write(("456\n").getBytes("utf-8"));
                        }catch (IOException e){
                            e.printStackTrace();
                        }
                    }
                }).start();
                break;
            case R.id.disconn:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            OutputStream os = null;
                            os = socket.getOutputStream();//得到socket的输出流
                            //输出EditText里面的数据,数据最后加上换行符才可以让服务器端的readline()停止阻塞
                            os.write(("789\n").getBytes("utf-8"));
                        }catch (IOException e){
                            e.printStackTrace();
                        }
                    }
                }).start();
                running = false;
                setButtonOnStartState(true);
                try {
                    socket.close();
                }catch (NullPointerException e){
                    e.printStackTrace();
                    disPlayToast("断开连接");
                }catch (IOException e){
                    e.printStackTrace();
                }
                break;
        }
    }

    private class StartThread extends Thread{
        @Override
        public void run(){
            try {
                int portnum = Integer.parseInt(port.getText().toString());
                socket = new Socket(IPet.getText().toString(),portnum);
                System.out.println(IPet.getText());
                rt = new ReceiveThread(socket);
                rt.start();
                running = true;
                System.out.println(socket.isConnected());
                if(socket.isConnected()){//成功连接,获取socket对象,发送成功消息
                    Message msg0 = myhandler.obtainMessage();
                    msg0.what = 0;
                    myhandler.sendMessage(msg0);
                    tv.setText("已连接ESP32");
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    private class ReceiveThread extends Thread{
        private InputStream is;
        public ReceiveThread(Socket socket) throws IOException{
            is = socket.getInputStream();
        }
        @Override
        public void run(){
            while (running){
                InputStreamReader isr = new InputStreamReader(is);
                BufferedReader br = new BufferedReader(isr);
                try {
                    //读取服务器端发过来的数据,阻塞一直到收到结束符\n\r
                    System.out.println(str = br.readLine());
                }catch (NullPointerException e){
                    running = false;
                    Message msg2 = myhandler.obtainMessage();
                    msg2.what = 2;
                    myhandler.sendMessage(msg2);
                    e.printStackTrace();
                    break;
                }catch (IOException e){
                    e.printStackTrace();
                }
                Message msg = myhandler.obtainMessage();
                msg.what = 1;
                msg.obj = str;
                myhandler.sendMessage(msg);
                try{
                    sleep(400);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
            Message msg2 = myhandler.obtainMessage();
            msg2.what = 2;
            myhandler.sendMessage(msg2);
        }

    }

    //设置按钮状态
    private void setButtonOnStartState(boolean flag) {
        conn.setEnabled(flag);
        disconn.setEnabled(!flag);
        on.setEnabled(!flag);
        off.setEnabled(!flag);
    }
    private void disPlayToast(String s){
        Toast.makeText(this,s,Toast.LENGTH_SHORT).show();
    }

    class MyHandler extends Handler{//在主线程中处理Handler传送回来的额message
        @Override
        public void handleMessage(Message msg){
            switch (msg.what){
                case 1:
                    String str = (String) msg.obj;
                    System.out.println(msg.obj);
                    tv.setText(str);
                    break;
                case 0:
                    disPlayToast("连接成功");
                    break;
                case 2:
                    disPlayToast("服务器已断开");
                    tv.setText(null);
                    setButtonOnStartState(true);
                    break;
            }
        }
    }

}

思路介绍完之后,附上giteeESP32 IOT APPicon-default.png?t=N7T8https://gitee.com/cheplus/esp32-iotapp

ESP32端

通过C语言配置,platformio

使用的屏幕是TFT 240*240(ST7789驱动)

配置页如下(.pio\libdeps\upesy_wroom\TFT_eSPI\User_Setups\Setup24_ST7789.h)

#define TFT_MISO -1
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS    -1 // Not connected
#define TFT_DC    2
#define TFT_RST   15  // Connect reset to ensure display initialises
#include <SPI.h>
#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h> // Hardware-specific library
#include <qrcode_espi.h>

TFT_eSPI tft = TFT_eSPI(); // Invoke custom library
const char *AP_SSID = "Redmi K30S Ultra";
const char *AP_Password = "qwerty123";
WiFiServer esp32_server(8080);
QRcode_eSPI qrcode(&tft);

void setup(void)
{
  tft.init();
  pinMode(2, OUTPUT);
  pinMode(13, OUTPUT);
  tft.fillScreen(0xffa500);

  Serial.begin(115200);
  delay(500);
  Serial.println("111111111111");
  WiFi.begin(AP_SSID, AP_Password); // 设置AP模式热点的名称和密码
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(5000);
    Serial.println("正在连接");
    tft.println("正在连接");
  }
  Serial.println("连接成功");
  tft.setCursor(0, 0, 4);
  // Set "cursor" at top left corner of display (0,0) and select font 4
  tft.setTextColor(TFT_BLUE);
  tft.println("Connected");
  tft.println(WiFi.localIP());
  qrcode.init();
  // create qrcode
  //delay(50000);
  qrcode.create(WiFi.localIP().toString().c_str());
  esp32_server.begin();
}

void loop()
{
  WiFiClient client = esp32_server.available();
  String msgs = "";
  if (client)
  { 
    if(client.connected()){
      tft.fillScreen(0x42a5f5);
      tft.setTextColor(TFT_WHITE);
      tft.println("client Connected");
    }
    while (client.connected())
    {
      // while (client.available() > 0)
      // {                                         
      //   char c = client.read();
      //   Serial.println(c);
      //   msgs += c;
      // }
      if (client.available()) //如果有数据可读取
            {
                String line = client.readStringUntil('\n'); //读取数据到换行符
                Serial.print("读取到数据:");
                Serial.println(line);
                msgs = line + "\n";
                client.write(msgs.c_str()); //将收到的数据回发
                //tft.println(line.c_str());
                if(line=="123"){
                  digitalWrite(2, HIGH);
                  digitalWrite(13, HIGH);
                }else if(line=="456"){digitalWrite(2, LOW);digitalWrite(13, LOW);}
            }
      //client.print(msgs); // 将socket客户端接收的数据发送回去
      
      msgs = "";
      delay(10);
    }
    client.stop();
    qrcode.create(WiFi.localIP().toString().c_str());
    Serial.println("Client disconnected");
  }
}

这个程序就相对简单,附上gitee

ESP32 IOT TFTicon-default.png?t=N7T8https://gitee.com/cheplus/esp32-iottft实物图

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值