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的回答: