打算做个远程图传+底盘控制的物联网设备,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芯片制作通过网页远程获取图像并实时操控方向的小车
ESP32-CAM AI-Thinker引脚指南:GPIO使用说明
esp32 multi task: Blink with Hello
使用Arduino开发ESP32(十):TCP Client