Android学习制作app(ESP8266-01S连接-简单制作)

一、理论

部分理论见arduino学习-CSDN博客Android Studio安装配置_android studio gradle 配置-CSDN博客

以下直接上代码和效果视频,esp01S的收发硬件代码目前没有分享,但是可以通过另一个手机网络调试助手进行模拟。也可以直接根据我的代码进行改动自行使用,代码中已经对模块进行了详细注释。本人不是java开发专业人士,也是通过ai完成的。

使用以下文件需要完成AndroidStdio的安装和SDK,SDK插件、gradle的配置,详细可以见之前的文章。

1、主xml文件制作界面

通过linearlayout布局,制作简单的界面,app头部为标题,中间为按钮和text显示。

复制代码
<?xml version="1.0" encoding="utf-8"?>
<!-- CYA开发,SmartOrderDishes内容,VX:18712214828 -->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
<!--    头部-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_weight="1"
        android:layout_height="match_parent"
        android:gravity="top"
        android:orientation="horizontal">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:text="SmartOrderDishes"
                android:background="#609E9245"
                android:gravity="center|left"
                android:paddingLeft="30dp"
                android:textSize="20sp"
                android:textStyle="bold"
                android:letterSpacing="0.2"
                android:drawableStart="@mipmap/ic_launcher"
                />
    </LinearLayout>
<!--    显示模块-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_weight="0.5"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
            <TextView
                android:layout_width="400dp"
                android:layout_height="match_parent"
                android:text="在连接ESP-01S WIFI后,等待LCD1602显示CanConnectServer。点击连接按钮,连接服务器"
                android:textSize="20dp"
                android:gravity="left"/>
        </LinearLayout>
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
        </LinearLayout>
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center">
            <Button
                android:onClick="Connect"
                android:layout_width="120dp"
                android:layout_height="60dp"
                android:layout_marginLeft="10dp"
                android:text="连接"
                />
            <Button
                android:onClick="OffConnect"
                android:layout_width="120dp"
                android:layout_height="60dp"
                android:layout_marginLeft="10dp"
                android:text="断开连接"
                />
        </LinearLayout>
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingTop="50dp">
            <TextView
                android:id="@+id/Show_Text"
                android:layout_width="wrap_content"
                android:layout_height="50dp"
                android:textSize="20sp"
                android:text="Wait Checking out!"
                android:gravity="center"/>
        </LinearLayout>
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:gravity="center"
        android:orientation="horizontal"></LinearLayout>
</LinearLayout>

2、主xml对应的java文件

此文件中,对socket连接和收发线程进行了使用,并且有两个按钮点击事件,和接收到服务器数据的弹窗和弹窗按钮点击事件。

复制代码
package com.example.smartorderdishes;
/*
CYA开发,VX:18712214828
自动点餐系统安卓app:
1、主线程进行点击时间和线程侦听
2、手机连接ESP-01S的WIFI后点击连接即可连接ESP服务器。(通过8080端口和192.168.4.1默认服务器ip)
3、接收到数据后进行弹窗显示需要结算的桌面,和总金额。
4、弹窗中点击确定即可结算。ESP会受到数据包。*/
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";//主java文件TAG
    private SocketClient socketClient;//socket自定义库文件变量
    private TextView textView;//TextView标签变量
    @SuppressLint("MissingInflatedId")
    @Override
    protected void onCreate(Bundle savedInstanceState) {//主java文件函数,只会运行一次
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        socketClient = new SocketClient(this);//变量对象初始化
        textView = findViewById(R.id.Show_Text);//获取标签id

        // 设置数据接收回调// 连接成功后启动持续监听
        socketClient.setDataReceivedCallback(new SocketClient.DataReceivedCallback() {
            @Override
            public void onDataReceived(String data) {
                runOnUiThread(() -> {
                    byte[]  DataPacket = socketClient.hexStringToByteArray(data);
                    int deskNum = ((DataPacket[1]&0xF0)/16)+1;
                    int priceCount = (DataPacket[1]*256+DataPacket[2])&0x0FFF;
                    showDialog(data);
                });
            }
        });

    }
    // 连接按钮点击事件
    public void Connect(View view) {
        // 连接到服务器(内部会自动启动接收循环)
        socketClient.connectToServer();
    }


    //断开连接按钮点击事件
    public void OffConnect(View view) {
        // 关闭连接
        socketClient.closeConnection();
    }

    // 显示弹窗
    private void showDialog(String data) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);//新建弹窗对象
        byte[]  DataPacket = socketClient.hexStringToByteArray(data);//传入的数据转化为字节数组
        int deskNum = ((DataPacket[1]&0xF0)/16)+1;//桌号获取
        int priceCount = (DataPacket[1]*256+DataPacket[2])&0x0FFF;//总金额获取
        builder.setTitle("桌号"+deskNum+",结算请求:");//弹窗标题
        builder.setMessage("共计总金额$" + priceCount+"是否结算!");//弹窗信息
        // 确定按钮
        builder.setPositiveButton("确认结算", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {//确认按钮点击事件
                Toast.makeText(MainActivity.this, "You clicked OK", Toast.LENGTH_SHORT).show();
                //发送十六进制数据
                String hexData = "EBAAFF90"; //发送结算成功数据包
                socketClient.sendHexData(hexData);

                textView.setText("桌号:" + deskNum+"结算,总金额$"+priceCount+"\n");//显示
                /*textView.setText("桌号:" + deskNum+"结算,总金额$"+priceCount+"\n"+
                            "Data:"+data);*/
            }
        });

        // 取消按钮
        builder.setNegativeButton("取消结算", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Toast.makeText(MainActivity.this, "You clicked Cancel", Toast.LENGTH_SHORT).show();
            }
        });

        // 显示弹窗
        AlertDialog dialog = builder.create();
        dialog.show();
    }



}

3、socket连接服务器、侦听数据包和发送数据包线程,Java文件

复制代码
package com.example.smartorderdishes;

import android.content.Context;
import android.util.Log;
import android.widget.Toast;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SocketClient {

    private static final String SERVER_IP = "192.168.4.1";//连接的指定IP
    private static final int SERVER_PORT = 8080;//连接服务器的指定端口
    private static final int CONNECTION_TIMEOUT = 5000;//连接超时时间 ms
    private static final int READ_TIMEOUT = 5000; // 新增读取超时时间 ms

    private Socket socket;//socket变量
    private BufferedOutputStream out;//输出缓冲区变量
    private BufferedInputStream in;//输入缓冲区变量
    private Context context;//
    private ExecutorService executorService;//单线程 用于连接服务器
    private ExecutorService receiverExecutor; // 独立线程池用于接收数据

    public SocketClient(Context context) {
        this.context = context;
        executorService = Executors.newSingleThreadExecutor();//单线程的执行器服务(Executor Service),用于管理和调度任务的执行
        receiverExecutor = Executors.newSingleThreadExecutor(); // 独立线程池 线程用于接收数据
    }

    // 连接服务器(修改后的代码)
    public void connectToServer() {
        executorService.execute(() -> {//线程提交不需要返回结果的任务
            try {//异常抛出
                socket = new Socket();//socket对象
                socket.connect(new InetSocketAddress(SERVER_IP, SERVER_PORT), CONNECTION_TIMEOUT);//socket连接,指定地址、端口和超时时间
                socket.setSoTimeout(READ_TIMEOUT); // 设置读取超时
                out = new BufferedOutputStream(socket.getOutputStream());//发送缓冲区对象
                in = new BufferedInputStream(socket.getInputStream());//接收缓冲区对象

                runOnUiThread(() -> {//runOnUiThread() 是 Activity 类中的一个方法 ,用于在主线程执行代码
                    Toast.makeText(context, "Connected to server", Toast.LENGTH_SHORT).show();
                    Log.d("SocketClient", "Connected to server");
                });

                // 连接成功后启动接收循环
                startReceivingData();

            } catch (IOException e) {
                runOnUiThread(() -> {
                    Toast.makeText(context, "Failed to connect: " + e.getMessage(), Toast.LENGTH_SHORT).show();
                    Log.e("SocketClient", "Connection error: " + e.getMessage());
                });
            }
        });
    }

    // 发送十六进制数据
    public void sendHexData(String hexData) {
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                if (out != null && socket != null && !socket.isClosed()) {
                    try {
                        // 将十六进制字符串转换为字节数组
                        byte[] data = hexStringToByteArray(hexData);
                        out.write(data);//发送字节数组
                        out.flush();//发送完毕后,关闭发送
                        Log.d("SocketClient", "Sent (Hex): " + hexData);
                    } catch (IOException e) {
                        e.printStackTrace();
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(context, "Failed to send data: " + e.getMessage(), Toast.LENGTH_SHORT).show();
                            }
                        });
                    }
                } else {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(context, "Not connected to server", Toast.LENGTH_SHORT).show();
                        }
                    });
                }
            }
        });
    }

    // 接收数据包(0xEB 0xXX 0xXX 0x90)
    // 启动接收循环
    private void startReceivingData() {
        receiverExecutor.execute(() -> {//通过单线程执行器,所有提交的任务都会按顺序在一个单独的线程中执行。
            Log.d("SocketClient", "Starting receive loop");
            try {
                while (!Thread.currentThread().isInterrupted()//用于检查当前线程是否已被中断的方法
                        && socket != null//检查 Socket 对象是否已经被初始化且不为 null。这个检查通常用于确保在尝试使用 Socket 进行网络通信之前,它已经被正确创建和配置。
                        && !socket.isClosed()//检查 Socket 对象是否被关闭
                        && in != null) {//确保输入流(InputStream)对象已经被正确初始化且不为 null,避免潜在的 NullPointerException
                    byte[] buffer = new byte[1024];//存储获取的数据
                    int bytesRead;//存储获取的数据长度
                    try {
                        bytesRead = in.read(buffer); // 阻塞读取(但设置了超时),返回数组长度
                        if (bytesRead == -1) {//未读取到数据
                            Log.d("SocketClient", "Connection closed by server");
                            break;
                        }
                        String hexResponse = byteArrayToHexString(buffer, bytesRead);//转字节数组换为字符串
                        Log.d("SocketClient", "Received (Hex): " + hexResponse);
                        if (isValidDataPacket(buffer, bytesRead)) {//判断是否符合数据包格式
                            Log.d("SocketClient", "Valid packet received");
                            // 触发回调
                            if (dataReceivedCallback != null) {//回调接口变量是否为空
                                dataReceivedCallback.onDataReceived(hexResponse);//回调不为空则运行回调函数,回调接收到的hex字符串
                            }
                        }
                    } catch (SocketTimeoutException e) {
                        Log.d("SocketClient", "Read timeout, retrying...");
                        continue;
                    } catch (IOException e) {
                        Log.e("SocketClient", "Read error: " + e.getMessage());
                        break;
                    }
                }
            } finally {
                Log.d("SocketClient", "Exiting receive loop");
            }
        });
    }

    // 检查数据包是否符合 0xEB 0xXX 0xXX 0x90 格式
    private boolean isValidDataPacket(byte[] data, int length) {
        if (length < 4) {
            Log.d("SocketClient", "Invalid packet: length < 4");
            return false;
        }
        boolean isValid = (data[0] == (byte) 0xEB) && (data[3] == (byte) 0x90);
        Log.d("SocketClient", "Data validity: " + isValid);
        return isValid;
    }

    // 关闭连接
    public void closeConnection() {
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    if (out != null) out.close();
                    if (in != null) in.close();
                    if (socket != null) socket.close();//关闭socket连接
                    Log.d("SocketClient", "Connection closed");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    // 回调接口,用于接收数据
    /*onDataReceived 是一个常见的回调方法名称,
    通常用于在数据接收到时通知监听器或处理数据。
    这个方法一般定义在一个接口中,
    并由实现该接口的类提供具体的数据处理逻辑。*/
    public interface DataReceivedCallback {
        void onDataReceived(String data);
    }

    // 设置回调接口
    private DataReceivedCallback dataReceivedCallback;//回调接口变量,回调接口为自定义,在上面已定义
    public void setDataReceivedCallback(DataReceivedCallback callback) {
        this.dataReceivedCallback = callback;
    }

    // 在主线程中运行代码
    private void runOnUiThread(Runnable action) {
        new android.os.Handler(context.getMainLooper()).post(action);
    }

    // 将十六进制字符串转换为字节数组
    public byte[] hexStringToByteArray(String hex) {
        int len = hex.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
                    + Character.digit(hex.charAt(i + 1), 16));
        }
        return data;
    }

    // 将字节数组转换为十六进制字符串
    private String byteArrayToHexString(byte[] bytes, int length) {
        StringBuilder hex = new StringBuilder();
        for (int i = 0; i < length; i++) {
            hex.append(String.format("%02X", bytes[i]));
        }
        return hex.toString();
    }


}

4、app获取网络权限文件,以及启动文件配置文件

复制代码
<?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.INTERNET" /> <!-- 访问网络状态 -->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- 访问wifi状态 -->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

    <!-- 访问WiFi网络的信息 -->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <!-- 允许改变WiFi连接状态(如果需要的话) -->
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <!-- 从Android 6.0(API level 23)开始,获取WiFi信息也需要位置权限 -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <!-- 或者使用粗略的位置权限 -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <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:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.SmartOrderDishes"
        tools:targetApi="31">
        <!-- 配置Activity可启动输出权限 -->
        <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>

二、效果

说明:

1、esp8266-01S开启AP模式的多连接的Station模式 。设定端口为8080 ,默认ip应该是192.168.4.1 ,网络SSID(名称)为ESP-01S。这些里面目前是固定的,配置即ESP8266作为热点和服务器,手机连接ESP8266的WIFI,然后作为客户端手机连接到ESP8266的服务器,进行通信

2、手机连接ESP8266的wifi后。等待配置完毕,然后进行服务器连接,连接完成手机会有信息提醒。

3、连接完成后,esp8266给手机发送0xEB 0xXX 0xXX 0x90的数据包DATA[4](下标从0开始),其中DATA[1]的高4bit作为桌位,低12bit为总金额。

4、接收到数据后手机app进行弹窗,点击确定后手机app界面text改变。并且向ESP8266发送EBAAFF90(HEX)数据作为结账完成的标志数据包。

效果视频

智能点餐系统开发视频

代码详解

其他代码问题(个人理解):

首先执行主线程mainactivity.java内容,创建UI和监听按钮动作。在onCreate创建的生命周期

(只执行一次,设置了数据接收回调的动作内容)。在socketclient.java中定义了回调函数,数据发送函数,数据接收函数,数据处理函数,类对象线程池创建等。

当mainactivity.java点击连接按钮时,触发Connect方法,进行服务器连接,在socketclient.java中的连接方法connectToServer启动了receiverExecutor线程。

receiverExecutor线程有while,会在while内持续运行,当

这些情况,线程才会结束,即意外断开服务器连接或者手动断开连接,线程才会退出,如果 in.read(buffer) 没有数据可读,线程会阻塞(挂起),直到有数据到达或超时。在接收到正确的数据包时,会触发回调,会在receiverExecutor运行mainactivity内的程序(想在主线程运行内容需要使用runOnUiThread())。


看一下AI的回答:

相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J5 天前
从“Hello World“ 开始 C++
c语言·c++·学习