小猪学arduino—ESP32-CAM远程视频监控&底盘控制

打算做个远程图传+底盘控制的物联网设备,uno的板子没有联网功能,esp8266、esp32成为两个比较好的选择,因为esp32是8266的升级版,我们这里就直接采用esp32-cam板进行测试。

一、概述

安信可推出的ESP32专为移动设备、可穿戴电子产品和物联网应用而设计,具有业内高水平的低功耗性能,包括精细分辨时钟门控、省电模式和动态电压调整等。

例如在低功耗 IoT 传感器 Hub 应用场景中,ESP32 只有在特定条件下才会被周期性地唤醒。低占空比可以极大降低芯片的能耗。射频功率放大器的输出功率也可调节,以实现通信距离、数据率和功耗之间的最佳平衡。

1.ESP32-CAM模组

  • 模组拥有业内极富竞争力的小尺寸摄像头模组,该模块可以作为最小系统独立工作,尺寸仅为 27*40.5*4.5mm,深度睡眠电流最低达到 6mA;
  • 可广泛应用于各种物联网场合,适用于家庭智能设备、工业无线控制、无线监无线识别,无线定位系统信号以及其它物联网应用,是物联网应用的理想解决方案;
  • 采用 DIP 封装,直接插上底板即可使用,实现产品的快速生产,为客户提供高可靠性的连接方式,方便应用于各种物联网硬件终端场合;

1.1.特点

  • 体积超小的 802.11b/g/n Wi-Fi + BT/BLE SoC 模块
  • 采用低功耗双核 32 位 CPU,可作应用处理器
  • 主频高达 240MHz,运算能力高达 600 DMIPS
  • 内置 520 KB SRAM,外置 4M PSRAM ESP-32CAM
  • 支持 UART/SPI/I2C/PWM/ADC/DAC 等接口
  • 支持 OV2640 和 OV7670 摄像头,内置闪光灯控、QR
  • 支持图片 WiFI 上传
  • 支持 TF 卡
  • 支持多种休眠模式
  • 内嵌 Lwip 和 FreeRTOS
  • 支持 STA/AP/STA+AP 工作模式
  • 支持 Smart Config/AirKiss 一键配网
  • 支持串口本地升级和远程固件升级(FOTA)

1.2.功能框图

1.3.引脚定义

1.3.1.ESP32-CAM引脚:

CAM和SD卡启用时会占用ESP32的以下端口:

1.3.2.ESP32S引脚

1.3.3.ESP8266-12E引脚

*注:如果接电驱,建议使用的引脚为D1-io5、D2-io4、D5-io14、D6-io12、D7-io13。其他引脚要么被占用,要么在上电启动引导过程中时会被拉高或拉低,如果用来接电驱,在上电、reset、连线断开时会出现诡异的转动现象。

 

二、开发环境

1.添加管理地址

文件-首选项-开发板管理URL设置,添加以下url地址:

https://dl.espressif.cn/dl/package_esp32_index.json

2.添加esp32开发板

3.选择开发版和端口

开发板选择“AI Thinker ESP32-CAM”,端口根据插上后的实际端口选择即可。

4.示例代码

示例代码中摄像头模式改成安信可的esp32-cam,即放开CAMERA_MODEL_AI_THINKER的定义,注释掉默认的WROVER_KIT。同时需要修改连接的wifi用户名和密码:

//#define CAMERA_MODEL_WROVER_KIT // Has PSRAM
//#define CAMERA_MODEL_ESP_EYE // Has PSRAM
//#define CAMERA_MODEL_M5STACK_PSRAM // Has PSRAM
//#define CAMERA_MODEL_M5STACK_V2_PSRAM // M5Camera version B Has PSRAM
//#define CAMERA_MODEL_M5STACK_WIDE // Has PSRAM
//#define CAMERA_MODEL_M5STACK_ESP32CAM // No PSRAM
#define CAMERA_MODEL_AI_THINKER // Has PSRAM
//#define CAMERA_MODEL_TTGO_T_JOURNAL // No PSRAM

...

const char* ssid = "MARS-4G";
const char* password = "xxxx";

...

void setup() {
  Serial.begin(115200);
...

5.代码上传

第一次用esp32烧写的同学,一定会遇到这个错误:Failed to connect to ESP32: Timed out waiting for packet header:

原因为上传时,必须把IO0接到GND上,并按下esp32的复位按键。具体操作步骤为:点击上传,在出现connecting ……__的时候,先长按住烧录底座上的IO0键,然后再按一次esp32-cam卡槽背部的RST键,然后松开IO0键的按压,即可成功开始烧录。

按键位置如下图:左上-烧录底座IO0按键、右上背面为ESP32-CAM的RST按键

烧录成功:

三、WIFI图传

1.图传测试

打开串口监视器,波特率修改为代码里的115200,按一次esp32-cam的RST复位键,即可打印出wifi摄像头的url地址,可以看到esp32使用80端口启动一个web服务,使用81端口传输视频流:

  • web服务(参数设置、视频播放):http://192.168.0.102
  • 视频流地址:http://192.168.0.102:81/stream

打开web服务url,点击Start Stream按钮,修改像素等参数,查看camera图像:

2.自定义视频页面

如果想单独显示视频,最简单的方法就是建个html页面,里边添加一个img即可:

<img id="stream" src="http://192.168.0.102:81/stream">

也可以直接jpage压缩后发送二进制流进行显示。

 

四、底盘控制

1.电驱连线

2.控制引脚

l298n的控制逻辑不再赘述,这里只对照上图连线给下控制引脚定义,后边用高低电平控制就可以了

注意:GPIO0用于烧录时接地;IO1/3用于TX/RX转USB通信;IO14/15/16/1端口空代码运行就会高电平,像是被什么占用了,没找到原因前先用剩下的io口。

// 马达引脚
int DC_LEFT1 = 3; // Left 1
int DC_LEFT2 = 2; // Left 2
int DC_RIGHT1 = 13; // Right 1
int DC_RIGHT2 = 12; // Right 2
int CAMERA_LED = 4; // Light灯

void setup() {
  ...
  // 初始化马达引脚
  pinMode(DC_LEFT1, OUTPUT);
  pinMode(DC_LEFT2, OUTPUT);
  pinMode(DC_RIGHT1, OUTPUT);
  pinMode(DC_RIGHT2, OUTPUT);
}

void loop() {
  ...
  digitalWrite(DC_LEFT1,HIGH); //给高电平
  //analogWrite(DC_LEFT1, speed); //pwm调速
  digitalWrite(DC_LEFT2,LOW); //给低电平
  digitalWrite(DC_RIGHT1,HIGH); //给高电平
  //analogWrite(DC_RIGHT1, speed); //pwm调速
  digitalWrite(DC_RIGHT2,LOW); //给低电平
  delay(10000);
}

3.远程测试

烧录代码,测试底盘电机运行是否正常。

 

五、4G/5G图传与控制

接下来我们需要能通过4G/5G网络给底盘发控制指令,我们采用mqtt的方式来控制:

1.MQTT通讯打通

1.1.mqtt broker

首先要准备好一个mqtt broker,为了快速调试,我这里使用百度云的iot core(虽然我觉得这个iot core一点也不好用,一堆限制,不如之前的iot hub灵活)。

1.2.mqtt client

安装官方mqtt client扩展和示例:https://github.com/arduino-libraries/ArduinoMqttClient

测试pub、sub topic:

/*
  Arduino Mqtt Client
*/

#include <ArduinoMqttClient.h>
#include <WiFi.h>

// wifi参数
char ssid[] = "MARS-4G";
char pass[] = "Lovezhu1314";


WiFiClient wifiClient;
MqttClient mqttClient(wifiClient);

// mqtt参数
const char broker[] = "amzvemj.iot.gz.baidubce.com";
int port = 1883;  //baidu iot core端口:TCP 1883 非加密连接; TLS/SSL 1884 基于TLS加密的连接; WSS 443 基于WebSocket及TLS的连接
const char pub_topic[]  = "tank1/cmd";
const char sub_topic[]  = "tank1/cmd"; // 注意:baidu iotcore的自定义topic不能以'/'开头!
const long interval = 1000;
unsigned long previousMillis = 0;

int count = 0;


void setup() {
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }

  // 连接wifi
  Serial.print("connect to WPA SSID: ");
  Serial.println(ssid);
  while (WiFi.begin(ssid, pass) != WL_CONNECTED) {
    Serial.print(".");  // retry
    delay(5000);
  }
  Serial.println("connected wifi success!\n");
  

  // 连接mqtt
  Serial.print("connect to the MQTT broker: ");
  Serial.println(broker);
  // unique client ID
  mqttClient.setId("esp32-tank1");
  // username and password for authentication
  //    username = {adp_type}@{IoTCoreId}|{DeviceKey}|{timestamp}|{algorithm_type}
  //    password = md5({device_key}&{timestamp}&{algorithm_type}{device_secret})
  mqttClient.setUsernamePassword("thingidp@amzvemj|tank1|0|MD5", "password");
  if (!mqttClient.connect(broker, port)) {
    Serial.print("MQTT connection failed! Error code = ");
    Serial.println(mqttClient.connectError());
    while (1);
  }
  Serial.println("connected MQTT broker success!\n");


  // 订阅topic
  Serial.print("Subscribing to topic: ");
  Serial.println(sub_topic);
  // message receive callback
  mqttClient.onMessage(onReceiveMessage);
  // subscribe to a topic
  mqttClient.subscribe(sub_topic);
  // mqttClient.unsubscribe(sub_topic);
  Serial.print("Waiting for messages on topic: ");
  Serial.println(sub_topic);
}

void loop() {
  // 接收消息
  mqttClient.poll();
  
  // 发送topic(1秒定频)
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;

    Serial.print("Sending message to topic: ");
    Serial.println(pub_topic);
    Serial.print("pub test ");
    Serial.println(count);

    // send message, the Print interface can be used to set the message contents
    mqttClient.beginMessage(pub_topic);
    mqttClient.print("pub test ");
    mqttClient.print(count);
    mqttClient.endMessage();

    Serial.println();

    count++;
  }
}


// 订阅消息的回调
void onReceiveMessage(int messageSize) {
  // we received a message, print out the topic and contents
  Serial.print("Received a message with topic '");
  Serial.print(mqttClient.messageTopic());
  Serial.print("', length ");
  Serial.print(messageSize);
  Serial.println(" bytes:");

  // use the Stream interface to print the contents
  while (mqttClient.available()) {
    Serial.print((char)mqttClient.read());
  }
  Serial.println();

  Serial.println();
}

执行情况:

2.通过MQTT指令控制底盘

2.1.单片机订阅指令

为了方便收发json数据,需要安装json库

订阅topic: tank1/cmd,接收控制指令{“x”:128, “y”:130},根据指令控制底盘:

...
#include <Arduino_JSON.h>
...

// 订阅消息的回调
void onReceiveMessage(int messageSize) {
  // print topic and contents
  Serial.print("sub msg: ");
  Serial.print(mqttClient.messageTopic());
  Serial.print(", length ");
  Serial.print(messageSize);
  Serial.print(" bytes, data: ");

  // get msg
  String msg = "";
  while (mqttClient.available()) {
    msg += (char)mqttClient.read();
  }
  Serial.println(msg);
  
  // json decode
  JSONVar cmd = JSON.parse(msg);
  //Serial.println(cmd);
  if (JSON.typeof(cmd) == "undefined") {
    Serial.println("json parsing failed!");
    //return;
  }
  int x = 128;
  int y = 128;
  if (cmd.hasOwnProperty("x")) {
    x = (int)cmd["x"];
  }
  if (cmd.hasOwnProperty("y")) {
    y = (int)cmd["y"];
  }

  // exec cmd
  execCommand(x, y);
}


// 执行底盘移动指令
void execCommand(int x, int y) {
  Serial.print("execCommand :");
  Serial.print(x);
  Serial.print(" ");
  Serial.println(y);

  //指令处理
  //  y 前进130-255,后退126-0, 不动128+-1
  //  x 左转126-0,右转130-255, 不动128+-1
  if (y >= 130 && y <= 255 && x >= 130 && x <= 255) { //前+右
    //Serial.println("UP RIGHT");
    //forward + right 向前右转
    //analogWrite(DC_LEFT1, DC_SPEED_LEFT);   //pwm调速
    digitalWrite(DC_LEFT2, LOW);
    //analogWrite(DC_RIGHT1, DC_SPEED_RIGHT * (255 - x)/(255-128));   //pwm调速
    digitalWrite(DC_RIGHT2, LOW);
  }
  else if (y >= 130 && y <= 255 && x >= 0 && x <= 126) { //前+左
    //Serial.println("UP LEFT");
    //forward + left 向前左转
    //analogWrite(DC_LEFT1, DC_SPEED_LEFT * (x - 0)/(128-0));   //pwm调速
    digitalWrite(DC_LEFT2, LOW);
    //analogWrite(DC_RIGHT1, DC_SPEED_RIGHT);   //pwm调速
    digitalWrite(DC_RIGHT2, LOW);
  }
  else if (y >= 0 && y <= 126 && x >= 0 && x <= 126) { //后+左
    //Serial.println("DOWN LEFT");
    //back + left 向左后转
    //digitalWrite(DC_LEFT1, LOW);
    //analogWrite(DC_LEFT2, DC_SPEED_LEFT);
    digitalWrite(DC_RIGHT1, LOW);
    //analogWrite(DC_RIGHT2, DC_SPEED_RIGHT * (x - 0)/(128-0));
  }
  else if (y >= 0 && y <= 126 && x >= 130 && x <= 255) { //后+右
    //Serial.println("DOWN RIGHT");
    //back + right 向右后转
    digitalWrite(DC_LEFT1, LOW);
    //analogWrite(DC_LEFT2, DC_SPEED_LEFT * (255 - x)/(255-128));
    digitalWrite(DC_RIGHT1, LOW);
    //analogWrite(DC_RIGHT2, DC_SPEED_RIGHT);
  }
  else if (y >= 130 && y <= 255) { //前
    Serial.println("Forward");
    //analogWrite(DC_LEFT1, DC_SPEED_LEFT);   //pwm调速
    digitalWrite(DC_LEFT1, HIGH); //给高电平
    digitalWrite(DC_LEFT2, LOW);
    //analogWrite(DC_RIGHT1, DC_SPEED_RIGHT);   //pwm调速
    digitalWrite(DC_RIGHT1, HIGH); //给高电平
    digitalWrite(DC_RIGHT2, LOW);
  }
  else if (y >= 0 && y <= 126) { //后
    Serial.println("Backward");
    digitalWrite(DC_LEFT1, LOW);
    digitalWrite(DC_LEFT2, HIGH); //给高电平
    //analogWrite(DC_LEFT2, DC_SPEED_LEFT);
    digitalWrite(DC_RIGHT1, LOW);
    digitalWrite(DC_RIGHT2, HIGH); //给高电平
    //analogWrite(DC_RIGHT2, DC_SPEED_RIGHT);
  }
  else if (x >= 0 && x <= 126) { //左
    Serial.println("TurnLeft");
    digitalWrite(DC_LEFT1, LOW);
    digitalWrite(DC_LEFT2, HIGH); //给高电平
    //analogWrite(DC_LEFT2, DC_SPEED_LEFT);
    //analogWrite(DC_RIGHT1, DC_SPEED_RIGHT);
    digitalWrite(DC_RIGHT1, HIGH); //给高电平
    digitalWrite(DC_RIGHT2, LOW);
  }
  else if (x >= 130 && x <= 255) { //右
    Serial.println("TurnRight");
    //analogWrite(DC_LEFT1, DC_SPEED_LEFT);   //pwm调速
    digitalWrite(DC_LEFT1, HIGH); //给高电平
    digitalWrite(DC_LEFT2, LOW);
    digitalWrite(DC_RIGHT1, LOW);
    digitalWrite(DC_RIGHT2, HIGH); //给高电平
    //analogWrite(DC_RIGHT2, DC_SPEED_RIGHT);
  }
  else if (y == 128 && x == 128) {  //停
    //Serial.println("Stop");
    digitalWrite(DC_LEFT1,LOW);
    digitalWrite(DC_LEFT2,LOW);
    digitalWrite(DC_RIGHT1,LOW);
    digitalWrite(DC_RIGHT2,LOW);
  }
}

远程指令接收测试(暂时先用web端发送指令进行验证):

2.2.手机端发送指令

这部分代码太长不放这里了,具体代码位置:https://github.com/yanjingang/study/tree/master/iot/mqtt/iotcore/ws-control

运行效果:

注:通过拖动下方的白球来生成横纵向控制指令,圆心为0,0,圆心右/上方向为正值,左/下方向为负值。

3.通过4G/5G传输图像

这里其实就是jpage压缩图片流发送到服务器转发给APP显示即可。需要注意的是,send camera stream阻塞会影响到sub cmd data的执行,如果网络延迟严重,可能会导致指令执行延迟。因此这些非控制部分则需要改为异步线程,这里我使用了freeROTS的xTaskCreate来创建异步线程任务,需要注意共享资源的互斥,避免并行资源抢占。

void loop(){
  //更新websocket信息
  webSocket.loop();
  uint64_t now = millis();
  if(now - lastSendStream > 100 && socketStatus && taskStatus==0) { //仅在连接ws 且 无其他异步任务执行时运行
      lastSendStream = now;
      /*// 同步推流
      camera_fb_t * fb = NULL;
      // Take Picture with Camera
      fb = esp_camera_fb_get();  
      if(!fb) {
        Serial.println("Camera capture failed");
        return;
      }else{
        webSocket.sendBIN(fb->buf, fb->len);
        Serial.print("Send Image to WsServer: len ");
        Serial.println(fb->len);
        esp_camera_fb_return(fb);
      }*/
      // 异步推流任务
      taskStatus = 1; //任务执行中
      xTaskCreate(
        &sendStream,   // Task function.
        "sendStream", // String with name of task.
        10240,     // Stack size in bytes.
        NULL,      // Parameter passed as input of the task
        1,         // Priority of the task.
        NULL);     // Task handle.
  }
}


//视频流推送服务器线程
void sendStream(void *parameter){
  camera_fb_t * fb = NULL;
  // Take Picture with Camera
  fb = esp_camera_fb_get();  
  if(!fb) {
    Serial.println("Camera capture failed");
  }else{
    if(socketStatus){ //ws已连接
      webSocket.sendBIN(fb->buf, fb->len);
      Serial.print("Send Image to WsServer: len ");
      Serial.println(fb->len);
    }
    esp_camera_fb_return(fb);
  }
  
  //vTaskDelay(100 / portTICK_RATE_MS);

  taskStatus=0; //当前任务结束
  
  vTaskDelete(NULL);
  
}

完整代码已上传:https://github.com/yanjingang/robot_remote_monitoring/

  • Esp32VideoTank:ESP32-CAM单片机程序,可以连接底盘或舵机进行控制

  • WebControlApp:JS写的视频显示和控制界面,通过4G网址或内嵌到微信小程序里使用

  • VideoStreamServer:视频流服务端,在云端服务器运行

运行效果:

  • 界面效果跟上一步一样,只是视频从局域网改为了云端服务器获取。
  • 视频像素和帧率、指令发送频率等要根据实际情况调试,调整到4G信号满格时,控制和视频流畅不卡顿

 

 

 

yan 22.7.16

 

参考:

利用ESP32-CAM芯片制作通过网页远程获取图像并实时操控方向的小车

esp32cam监控小车代码以及连线

JS制作游戏摇杆方向盘

esp32 的PWM实现

ESP32-CAM AI-Thinker引脚指南:GPIO使用说明

esp32推视频流到服务器

JS Blob与Base64互转

esp32 multi task: Blink with Hello

使用Arduino开发ESP32(十):TCP Client

ESP8266引脚的说明

欢迎关注下方“非著名资深码农“公众号进行交流~

发表评论

邮箱地址不会被公开。