1. 说明 Esp32Cam Tcp服务器
    android  客户端
    esp32  控制终端
    android  发送控制命令     接收Esp32Cam  图像视频 以及 Esp32Cam 和esp32 相关控制完成的反馈  
    Esp32Cam 向 app 发送视频 发送自己和esp32的控制完成反馈   通过串口向esp32转发app对esp32的控制指令      接收来自 app的控制指令和 esp32的串口数据
    esp32接收通过与esp32cam的串口通信获取的来自app的控制指令数据  完成相关操作  将结果通过串口反馈给esp32cam  然后转发到app
  2. app代码
package com.example.tcpclient_eap32cam_1025;


import androidx.appcompat.app.AppCompatActivity;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MainActivity extends AppCompatActivity {
    EditText host_editText,port_editText,send_data;
    TextView rec_data;
    Button connect_button,send;
    ImageView show_cam;
    Socket socket;
    InputStream inputStream;
    OutputStream outputStream;
    byte[] RevBuff = new byte[1024];  //定义接收数据流的包的大小
    MyHandler myHandler;
    byte[] temp = new byte[0];  //存放一帧图像的数据
    int headFlag = 0;    // 0 数据流不是图像数据   1 数据流是图像数据
    Bitmap bitmap = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        host_editText = findViewById(R.id.host_editText); //服务器地址
        port_editText = findViewById(R.id.port_editText);//服务器端口
        connect_button= findViewById(R.id.connect_button);//连接服务器按钮
        rec_data = findViewById(R.id.rec_data); //存放接收到的非图像数据
        send = findViewById(R.id.send);//发送数据按钮
        send_data = findViewById(R.id.send_data);//发送数据文本框
        connect_button.setText("连接"); //设置连接按钮名称为连接 如果已连接上显示断开
        show_cam = findViewById(R.id.show_cam); //存放图像数据
        myHandler = new MyHandler();
//        连接服务器操作
        connect_button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) {
                if(connect_button.getText() == "连接"){
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            Message msg = myHandler.obtainMessage();
                            try {
                                //如果 host_editText  port_editText为空的话 点击连接 会退出程序
                                socket = new Socket((host_editText.getText()).toString(),Integer.valueOf(port_editText.getText().toString()));
                                //socket = new Socket("192.168.0.3",8080);
                                if(socket.isConnected()){
                                    msg.what = 0;//显示连接服务器成功信息
                                    inputStream = socket.getInputStream();
                                    outputStream = socket.getOutputStream();
                                    Recv();//接收数据
                                }else{
                                    msg.what = 1;//显示连接服务器失败信息
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                                msg.what = 1;//显示连接服务器失败信息
                            }
                            myHandler.sendMessage(msg);
                        }
                    }).start();
                }else{
//                    关闭socket连接
                    try { socket.close(); } catch (IOException e) { e.printStackTrace(); }
                    try { inputStream.close(); }catch (IOException e) { e.printStackTrace(); }
                    try { outputStream.close(); }catch (IOException e) { e.printStackTrace(); }
                    connect_button.setText("连接");
                }
            }
        });
//        发送数据
        send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
//                            发送数据
                            outputStream.write(send_data.getText().toString().getBytes());
                        } catch (IOException e) {
//                            如果发送数据失败 显示连接服务器失败信息
                            e.printStackTrace();
                            Message msg = myHandler.obtainMessage();
                            msg.what = 1;
                            myHandler.sendMessage(msg);
                        }
                    }
                }).start();
            }
        });
    }
    //    接收数据方法
    public void Recv(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(socket != null && socket.isConnected()){
                    try {
                        int Len = inputStream.read(RevBuff);
                        if(Len != -1){
//                          图像数据包的头  FrameBegin
                            boolean begin_cam_flag = RevBuff[0] == 70 && RevBuff[1] == 114 && RevBuff[2] == 97 && RevBuff[3] == 109 && RevBuff[4] == 101
                                    && RevBuff[5] == 66 && RevBuff[6] == 101 && RevBuff[7] == 103 && RevBuff[8] == 105 && RevBuff[9] == 110 ;
//                            图像数据包的尾  FrameOverr
                            boolean end_cam_flag = RevBuff[0] == 70 && RevBuff[1] == 114 && RevBuff[2] == 97 && RevBuff[3] == 109 && RevBuff[4] == 101
                                    && RevBuff[5] == 79 && RevBuff[6] == 118 && RevBuff[7] == 101 && RevBuff[8] == 114 && RevBuff[9] == 114;
//                            判断接收的包是不是图片的开头数据 是的话s说明下面的数据属于图片数据 将headFlag置1
                            if(headFlag == 0 && begin_cam_flag){
                                headFlag = 1;
                            }else if(end_cam_flag){  //判断包是不是图像的结束包 是的话 将数据传给 myHandler  3 同时将headFlag置0
                                Message msg = myHandler.obtainMessage();
                                msg.what = 3;
                                myHandler.sendMessage(msg);
                                headFlag = 0;
                            }else if(headFlag == 1){ //如果 headFlag == 1 说明包是图像数据  将数据发给byteMerger方法 合并一帧图像
                                temp = byteMerger(temp,RevBuff);
                            }
//                            定义包头 Esp32Msg  判断包头 在向myHandler  2 发送数据    eadFlag == 0 && !end_cam_flag没用 会展示图像的数据
                            boolean begin_msg_begin = RevBuff[0] == 69 && RevBuff[1] == 115 && RevBuff[2] == 112 && RevBuff[3] == 51 && RevBuff[4] == 50
                                    && RevBuff[5] == 77 && RevBuff[6] == 115 && RevBuff[7] == 103 ;
                            if(begin_msg_begin){
                                Message msg = myHandler.obtainMessage();
                                msg.what = 2;
                                msg.arg1 = Len;
                                msg.obj = RevBuff;
                                myHandler.sendMessage(msg);
                            }
                        }else{
//                            如果Len = -1 说明接受异常  显示连接服务器失败信息  跳出循环
                            Message msg = myHandler.obtainMessage();
                            msg.what = 1;
                            myHandler.sendMessage(msg);
                            break;
                        }
                    } catch (IOException e) {
//                        如果接受数据inputStream.read(RevBuff)语句执行失败 显示连接服务器失败信息  跳出循环
                        e.printStackTrace();
                        Message msg = myHandler.obtainMessage();
                        msg.what = 1;
                        myHandler.sendMessage(msg);
                        break;
                    }
                }
            }
        }).start();
    }

    //    合并一帧图像数据  a 全局变量 temp   b  接受的一个数据包 RevBuff
    public byte[] byteMerger(byte[] a,byte[] b){
        int i = a.length + b.length;
        byte[] t = new byte[i]; //定义一个长度为 全局变量temp  和 数据包RevBuff 一起大小的字节数组 t
        System.arraycopy(a,0,t,0,a.length);  //先将 temp(先传过来的数据包)放进  t
        System.arraycopy(b,0,t,a.length,b.length);//然后将后进来的这各数据包放进t
        return t; //返回t给全局变量 temp
    }
    //处理一些不能在线程里面执行的信息
    class MyHandler extends Handler{
        public void handleMessage(Message msg){
            super.handleMessage(msg);
            switch (msg.what){
                case 0:
//                    连接服务器成功信息
                    Toast.makeText(MainActivity.this,"连接服务器成功!",Toast.LENGTH_SHORT).show();
                    connect_button.setText("断开");
                    break;
                case 1:
//                    连接服务器失败信息
                    Toast.makeText(MainActivity.this,"连接服务器失败!",Toast.LENGTH_SHORT).show();
                    break;
                case 2:
//                    处理接收到的非图像数据
                    byte[] Buffer = new byte[msg.arg1];
                    System.arraycopy((byte[])msg.obj,0,Buffer,0,msg.arg1);
                    SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");
                    Date date = new Date(System.currentTimeMillis());
                    String content = (new String(Buffer)) + "----"  + formatter.format(date) + "\n";
                    rec_data.append(content);
                    break;
                case 3:
//                    处理接受到的图像数据 并展示
                    bitmap = BitmapFactory.decodeByteArray(temp, 0,temp.length);
                    show_cam.setImageBitmap(bitmap);//这句就能显示图片(bitmap数据没问题的情况下) 存在图像闪烁情况 待解决
                    temp = new byte[0];  //一帧图像显示结束  将 temp清零
                    break;
                default: break;
            }
        }
    }
    //    销毁窗体 释放资源
    protected void onDestroy() {
        super.onDestroy();
        if(inputStream != null){
            try {inputStream.close();}catch(IOException e) {e.printStackTrace();}
        }

        if(outputStream != null){
            try {outputStream.close();} catch (IOException e) {e.printStackTrace();}
        }
        if(socket != null){
            try {socket.close();} catch (IOException e) {e.printStackTrace();}
        }
    }
}
<uses-permission android:name="android.permission.INTERNET" />




<?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:orientation="vertical"
    tools:context=".MainActivity"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:orientation="horizontal"
        tools:context=".MainActivity"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginBottom="4dp">
        <EditText
            android:layout_width="160dp"
            android:layout_height="50dp"
            android:id="@+id/host_editText"
            android:hint="服务器地址"
            android:selectAllOnFocus="true"
            android:inputType="phone"/>
        <EditText
            android:layout_width="160dp"
            android:layout_height="50dp"
            android:id="@+id/port_editText"
            android:hint="端口"
            android:inputType="phone"/>
        <Button
            android:id="@+id/connect_button"
            android:layout_width="80dp"
            android:layout_height="50dp"
            android:text="连接" />
    </LinearLayout>

    <ImageView
        android:id="@+id/show_cam"
        android:layout_width="match_parent"
        android:layout_height="220dp"
        android:background="#333"
        android:scaleType="center"
        android:layout_marginBottom="4dp"/>
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="320dp">
        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:id="@+id/rec_data"
            android:hint="接收消息"/>
    </ScrollView>

    <EditText
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:id="@+id/send_data"
        android:layout_marginBottom="4dp"
        android:hint="发送消息"/>
    <Button
        android:text="发送"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:id="@+id/send" />

</LinearLayout>
  1. esp32代码
#include <Arduino.h>
String recv_data = ""; //接受串口数据的变量
String esp32_head = "esp32_con_head:";  //esp32与服务器的通信标识头
//esp32cam 串口向 esp32发送消息的标识头 主要是联网信息 摄像头初始化信息等 
// 这些信息 esp32显示在串口或小屏幕上
String esp32cam_to_esp32 = "esp32cam_to_esp32:";
const int LED = 2;
void setup() {
  Serial.begin(115200);
  Serial2.begin(115200);
  pinMode(LED,OUTPUT);
  digitalWrite(LED, LOW);
}

void loop() {
  if(Serial2.available()){
    recv_data = Serial2.readStringUntil('\n');
    Serial.println(recv_data);
    if(recv_data.length() > 10){
      if(recv_data.substring(0,esp32_head.length()) == esp32_head){
        //消息来自服务器端 处理相关指令 必须这样判断 防止存在 \r\n等看不见的字符
        if(recv_data.substring(0,(esp32_head + "OpenEsp32Led").length()) == (esp32_head + "OpenEsp32Led") ){
          digitalWrite(LED, HIGH);
          Serial2.println( esp32_head + "Led ON!"); //串口发送给ESPCAM ESPCAM在发送给服务器
        }
        if(recv_data.substring(0,(esp32_head + "CloseEsp32Led").length()) == (esp32_head + "CloseEsp32Led")  ){
          digitalWrite(LED, LOW);
          Serial2.println( esp32_head + "Led OFF!");
        }
      }
      if(recv_data.substring(0,esp32cam_to_esp32.length()) == esp32cam_to_esp32){
        //消息来自esp32cam的数据一般为一些提示信息 可以用在串口或屏幕起到提示作用
        Serial.println(recv_data);
      }
    }else{
      Serial.println("Error Message!!!");
    }
  }
}
  1. esp32cam代码
#include <Arduino.h>
#include <WiFi.h>
#include "esp_camera.h"
#include <vector>

#define maxcache 1024  //图像数据包的大小

const char* ssid = "****";
const char* password = "******";

const int LED = 4;//闪光灯
const int ZHESHI_LED = 33; //指示灯 
bool cam_state = true;  //是否开启摄像头传输
const int port = 8080;
String  frame_begin = "FrameBegin"; //图像传输包头
String  frame_over = "FrameOverr";  //图像传输包尾
String  msg_begin = "Esp32Msg";  //传输给服务器的消息传输头  服务器用来判断是文本数据而不是图像数据
String esp32_head = "esp32_con_head:";  //服务器与esp32通信标识头 
String esp32_cam_head = "esp32_cam_head:"; //服务器与esp32cam的通信标识头
//esp32cam 串口向 esp32发送消息的标识 发送的消息主要是联网信息 摄像头初始化信息等 
// 这些信息 esp32用来显示在串口或小屏幕上 起到提醒作用
String esp32cam_to_esp32 = "esp32cam_to_esp32:"; 
bool loop_begin = true;//loop循环执行的条件 网络服务器OK了 才执行loop
bool camera_status = true;//摄像头状态 true能用 false出现故障 不能用了
//创建服务器端
WiFiServer server;
//创建客户端
WiFiClient client;

//CAMERA_MODEL_AI_THINKER类型摄像头的引脚定义
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
 
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

static camera_config_t camera_config = {
    .pin_pwdn = PWDN_GPIO_NUM,
    .pin_reset = RESET_GPIO_NUM,
    .pin_xclk = XCLK_GPIO_NUM,
    .pin_sscb_sda = SIOD_GPIO_NUM,
    .pin_sscb_scl = SIOC_GPIO_NUM,
    
    .pin_d7 = Y9_GPIO_NUM,
    .pin_d6 = Y8_GPIO_NUM,
    .pin_d5 = Y7_GPIO_NUM,
    .pin_d4 = Y6_GPIO_NUM,
    .pin_d3 = Y5_GPIO_NUM,
    .pin_d2 = Y4_GPIO_NUM,
    .pin_d1 = Y3_GPIO_NUM,
    .pin_d0 = Y2_GPIO_NUM,
    .pin_vsync = VSYNC_GPIO_NUM,
    .pin_href = HREF_GPIO_NUM,
    .pin_pclk = PCLK_GPIO_NUM,
    
    .xclk_freq_hz = 20000000,
    .ledc_timer = LEDC_TIMER_0,
    .ledc_channel = LEDC_CHANNEL_0,
    
    .pixel_format = PIXFORMAT_JPEG,
    .frame_size = FRAMESIZE_VGA,
    .jpeg_quality = 31,   //图像质量   0-63  数字越小质量越高
    .fb_count = 1,
};
//初始化摄像头
esp_err_t camera_init() {
    //initialize the camera
    esp_err_t err = esp_camera_init(&camera_config);
    if (err != ESP_OK) {
        Serial.println(esp32cam_to_esp32 + "Camera Init Failed!");
        camera_status = false;
        return err;
    }
    sensor_t * s = esp_camera_sensor_get();
    //initial sensors are flipped vertically and colors are a bit saturated
    if (s->id.PID == OV2640_PID) {
    //        s->set_vflip(s, 1);//flip it back
    //        s->set_brightness(s, 1);//up the blightness just a bit
    //        s->set_contrast(s, 1);
    }
    Serial.println(esp32cam_to_esp32 + "Camera Init OK!");
    camera_status = true;
    return ESP_OK;
}

bool wifi_init(const char* ssid,const char* password ){
  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false); //关闭STA模式下wifi休眠,提高响应速度
  #ifdef staticIP
    WiFi.config(staticIP, gateway, subnet);
  #endif
  WiFi.begin(ssid, password);
  uint8_t i = 0;
  while (WiFi.status() != WL_CONNECTED && i++ < 20) {
      delay(500);
  }
  if (i == 21) {
    Serial.println(esp32cam_to_esp32 + "Could not connect to" + ssid); 
    digitalWrite(ZHESHI_LED,HIGH);  //网络连接失败 熄灭指示灯
    return false;
  }
  Serial.println(esp32cam_to_esp32 + "Connecting to wifi " + ssid + " success!"); 
  digitalWrite(ZHESHI_LED,LOW);  //网络连接成功 点亮指示灯
  return true;
}

void TCPServerInit(){
  //启动server
  server.begin(port);
  //关闭小包合并包功能,不会延时发送数据
  server.setNoDelay(true);
  Serial.print(esp32cam_to_esp32 + "Ready! TCP Server: ");
  Serial.print(WiFi.localIP());
  Serial.print(":8080 Running!\n");
}
void cssp(){
  camera_fb_t * fb = esp_camera_fb_get();
  uint8_t * temp = fb->buf; //这个是为了保存一个地址,在摄像头数据发送完毕后需要返回,否则会出现板子发送一段时间后自动重启,不断重复
  if (!fb)
  {
    camera_status = false;
    Serial.println(esp32cam_to_esp32 +  "Camera Capture Failed");
  }
  else
  { 
    //先发送Frame Begin 表示开始发送图片 然后将图片数据分包发送 每次发送1430 余数最后发送 
    //完毕后发送结束标志 Frame Over 表示一张图片发送完毕 
    client.print(frame_begin); //一张图片的起始标志
    // 将图片数据分段发送
    int leng = fb->len;
    int timess = leng/maxcache;
    int extra = leng%maxcache;
    for(int j = 0;j< timess;j++)
    {
      client.write(fb->buf, maxcache); 
      for(int i =0;i< maxcache;i++)
      {
        fb->buf++;
      }
    }
    client.write(fb->buf, extra);
    client.print(frame_over);      // 一张图片的结束标志
    //Serial.print("This Frame Length:");
    //Serial.print(fb->len);
    //Serial.println(".Succes To Send Image For TCP!");
    //return the frame buffer back to the driver for reuse
    fb->buf = temp; //将当时保存的指针重新返还
    esp_camera_fb_return(fb);  //这一步在发送完毕后要执行,具体作用还未可知。        
  }
  //delay(20);//短暂延时 增加数据传输可靠性        
}
void TCPServerMonitor(){
if (server.hasClient()) {
  if ( client && client.connected()) {
    WiFiClient serverClient = server.available();
    serverClient.stop();
    Serial.println(esp32cam_to_esp32 + "Connection rejected!");
  }else{
    //分配最新的client
    client = server.available();
    client.println(msg_begin +  "Client is Connect!");
    Serial.println(esp32cam_to_esp32 + "Client is Connect!");
  }
}
// 读取串口esp32数据 转发给服务器
  if(Serial.available())
  {
    String esp32_data = Serial.readStringUntil('\n');
    client.println(msg_begin +  esp32_data);
  }
  //检测client发过来的数据
if (client && client.connected()) {
  if (client.available()) {
    String line = client.readStringUntil('\n'); //读取数据到换行符
    // 如果数据是服务器发送给esp32的 则通过串口发给esp32
    if (line.substring(0,esp32_head.length()) == esp32_head)
    {
      Serial.println(line);
    }else if (line.substring(0,esp32_cam_head.length()) == esp32_cam_head) 
    {// 如果数据是服务器发送给esp32cam的 则 根据指令处理相关逻辑
      if (line == esp32_cam_head + "CamOFF"){
        cam_state = false;
        client.println(msg_begin +  "Camera OFF!");
      }
      if (line == esp32_cam_head + "CamON"){
        cam_state = true;
        client.println(msg_begin +  "Camera ON!");
      }
      if (line == esp32_cam_head + "LedOFF"){
        digitalWrite(LED, LOW);
        client.println(msg_begin +  "Led OFF!");
      }
      if (line == esp32_cam_head + "LedON"){
        digitalWrite(LED, HIGH);
        client.println(msg_begin +  "Led ON!");
      }
    }else{
      client.println(msg_begin +  "Error Message!");
    }
  }
}
  
// 视频传输
if(camera_status && cam_state)
{
  if (client && client.connected()) {
    cssp();
  }
}
}

void setup() {
  Serial.begin(115200);
  pinMode(ZHESHI_LED, OUTPUT);
  digitalWrite(ZHESHI_LED, HIGH);
  pinMode(LED, OUTPUT);
  digitalWrite(LED, LOW);
  if(wifi_init(ssid,password) && camera_init() == 0){
    TCPServerInit();
  }else{
    loop_begin = false;
  }
}

void loop() {
  if(loop_begin){
    TCPServerMonitor();
  }
}