- 实现结果
 - app可以控制Esp32Cam的摄像头开关和闪光灯的开关
 - 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"; //消息传输头
//创建服务器端
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("Camera Init Failed!");
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("Camera Init OK!");
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;
Serial.println();
while (WiFi.status() != WL_CONNECTED && i++ < 20) {
delay(500);
Serial.print(".");
}
if (i == 21) {
Serial.println();
Serial.print("Could not connect to");
Serial.println(ssid);
digitalWrite(ZHESHI_LED,HIGH); //网络连接失败 熄灭指示灯
return false;
}
Serial.print("Connecting to wifi ");
Serial.print(ssid);
Serial.println(" success!");
digitalWrite(ZHESHI_LED,LOW); //网络连接成功 点亮指示灯
return true;
}
void TCPServerInit(){
//启动server
server.begin(port);
//关闭小包合并包功能,不会延时发送数据
server.setNoDelay(true);
Serial.print("Ready! TCP Server");
Serial.print(WiFi.localIP());
Serial.println(":8080 Running!");
}
void cssp(){
camera_fb_t * fb = esp_camera_fb_get();
uint8_t * temp = fb->buf; //这个是为了保存一个地址,在摄像头数据发送完毕后需要返回,否则会出现板子发送一段时间后自动重启,不断重复
if (!fb)
{
Serial.println("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("Connection rejected!");
}else{
//分配最新的client
client = server.available();
client.println(msg_begin + "Client is Connect!");
Serial.println("Client is Connect!");
}
}
//检测client发过来的数据
if (client && client.connected()) {
if (client.available()) {
String line = client.readStringUntil('\n'); //读取数据到换行符
if (line == "CamOFF"){
cam_state = false;
client.println(msg_begin + "Camera OFF!");
}
if (line == "CamON"){
cam_state = true;
client.println(msg_begin + "Camera ON!");
}
if (line == "LedOFF"){
digitalWrite(LED, LOW);
client.println(msg_begin + "Led OFF!");
}
if (line == "LedON"){
digitalWrite(LED, HIGH);
client.println(msg_begin + "Led ON!");
}
Serial.println(line);
}
}
// 视频传输
if(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);
wifi_init(ssid,password);
camera_init();
TCPServerInit();
}
void loop() {
TCPServerMonitor();
}
- 安卓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();}
}
}
}
<?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>
<uses-permission android:name="android.permission.INTERNET" />
?
|