- 说明
Esp32Cam Tcp服务器
android 客户端
esp32 控制终端android 发送控制命令 接收Esp32Cam 图像视频 以及 Esp32Cam 和esp32 相关控制完成的反馈
Esp32Cam 向 app 发送视频 发送自己和esp32的控制完成反馈 通过串口向esp32转发app对esp32的控制指令 接收来自 app的控制指令和 esp32的串口数据esp32接收通过与esp32cam的串口通信获取的来自app的控制指令数据 完成相关操作 将结果通过串口反馈给esp32cam 然后转发到app
-
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>
-
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!!!"); } } }
-
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(); } }