FireBeetle 2 ESP32-S3 自制监控并接入HomeAssistant
本例实现将Firebeetle CAM 接入到HomeAssistant中,并通过舵机控制摄像头角度,定时或按需求获取摄像头画面,并将照片保存在SD卡中。1.HA简介HomeAssistant简称HA,是用于家庭自动化的免费开源软件,旨在成为智能家居设备的中央控制系统,是控制物联网由模块化集成组件支持的连接技术设备、软件、应用程序和服务,包括用于蓝牙、Zigbee和Z-Wave等无线通信协议的本机集成组件,通关专用API或MQTT等方式提供公共访问,以便通过局域网或互联网进行第三方集成,互联互通各家智能设备,如小米,涂鸦,YeeLight等,更是支持DIY智能硬件接入,联动整个智能生态。HA 的安装和部署链接https://www.home-assistant.io/installation/,想要体验的小伙伴自行解决。
2.硬件连接SD卡:注意SD卡模块供电是5VTFT屏幕:舵机:注意舵机需要单独供电
3.本例用到ArduinoHA库,TFT_eSPI显示库,JPEGDecoder解码库,ESPServo舵机库ArduinoHA 链接https://github.com/dawidchyrzynski/arduino-home-assistantJPEGDecoder 链接https://github.com/Bodmer/JPEGDecoderTFT_eSPI 链接https://github.com/Bodmer/TFT_eSPIESP32Servo 链接https://madhephaestus.github.io/ESP32Servo/annotated.html
4.代码部分#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <WiFi.h>
#include <ArduinoHA.h>
#include <PubSubClient.h>
#include <pins_arduino.h>
#include <FFat.h>
#include <LittleFS.h>
#include <SPIFFS.h>
#include <SD.h>
#include <EEPROM.h>
#include "esp_camera.h"
#include "DFRobot_AXP313A.h"
#include <ESP32Servo.h>
#include <pins_arduino.h>
#include <JPEGDecoder.h>
#include <TFT_eSPI.h>
#define FIREBEETLE_S3_PSRAM
#include "camera_pins.h"
// mtqq 服务器地址
#define BROKER_ADDR IPAddress(192, 168, 123, 209)
// 推送和保存的时间间隔
#define INTERVAL 20000
DFRobot_AXP313A axp;
WiFiClient client;
HADevice device;
HAMqtt mqtt(client, device);
HACamera haCamera("myCamera");
HAButton button("button");
HANumber number1("NumberX", HANumber::PrecisionP0);
HANumber number2("NumberY", HANumber::PrecisionP0);
SPIClass sdspi;
TFT_eSPI tft = TFT_eSPI();
unsigned long lastPublishAt = 0;
uint32_t pic_cnt;
volatile bool takepic = false;
const int servoXPin = A4, servoYPin = A5;
Servo servoX;
Servo servoY;
void startCameraServer();
void setupCamera();
void setupWiFi(const char *ssid, const char *password);
void publishCameraImage();
void setupHA();
void callback(const char *topic, const uint8_t *payload, uint16_t length);
void writeFile(fs::FS &fs, const char *path, uint8_t *data, size_t len);
void showTime(uint32_t msTime);
void jpegInfo();
void jpegRender(int xpos, int ypos);
void drawSdJpeg(camera_fb_t *fb, int xpos, int ypos);
void showTime(uint32_t msTime);
void setupSD();
void setupServo();
void onNumberCommand(HANumeric number, HANumber *sender);
void onButtonCommand(HAButton *sender);
void setup()
{
Serial.begin(115200);
while (!Serial)
;
Serial.setDebugOutput(true);
// 初始化tft屏幕
tft.init();
tft.setRotation(3);
while (axp.begin() != 0)
{
Serial.println("init error");
delay(1000);
}
// 设置摄像头供电
axp.enableCameraPower(axp.eOV2640);
// 初始化 Camera
setupCamera();
// 连接到wifi
setupWiFi("ShuangYY", "334452000");
// 配置HA
setupHA();
setupSD();
EEPROM.begin(4);
pic_cnt = EEPROM.readUInt(0);
}
void loop()
{
mqtt.loop();
// 返回摄像头照片
camera_fb_t *fb = esp_camera_fb_get();
// 读取失败
if (!fb)
{
return;
}
if (millis() - lastPublishAt > INTERVAL || takepic)
{
if (takepic == true)
takepic = false;
lastPublishAt = millis();
publishCameraImage_and_take_photo(pic_cnt, fb);
pic_cnt++;
EEPROM.writeUInt(0, pic_cnt);
}
// 解码 JEGP 图片
drawSdJpeg(fb, 0, 0);
// 释放缓存
esp_camera_fb_return(fb);
}
// 配置 摄像头
void setupCamera()
{
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.frame_size = FRAMESIZE_HVGA;
config.pixel_format = PIXFORMAT_JPEG; // for streaming
// config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 1;
if (psramFound())
{
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
Serial.println("PSRAM 启用成功");
}
else
{
// Limit the frame size when PSRAM is not available
config.frame_size = FRAMESIZE_SVGA;
config.fb_location = CAMERA_FB_IN_DRAM;
Serial.println("PSRAM 启用失败");
}
// camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK)
{
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
}
// 接入WiFi
void setupWiFi(const char *ssid = "ShuangYY", const char *password = "334452000")
{
WiFi.begin(ssid, password);
WiFi.setSleep(false);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
startCameraServer();
Serial.print("Camera Ready! Use 'http://");
Serial.print(WiFi.localIP());
Serial.println("' to connect");
}
// 推送数据到 mqtt 服务器,并保存到 SD 卡
void publishCameraImage_and_take_photo(int n, camera_fb_t *fb)
{
// 将照片推送到HA
haCamera.publishImage(fb->buf, fb->len);
char filename;
sprintf(filename, "/image%d.jpg", n);
// 将照片写入到SD卡
writeFile(SD, filename, fb->buf, fb->len);
}
// 配置HA的参数
void setupHA()
{
byte mac;
WiFi.macAddress(mac);
device.setUniqueId(mac, sizeof(mac));
// set device's details (optional)
device.setName("FireBettle2-CAM");
device.setSoftwareVersion("1.2.3");
device.setManufacturer("DIY");
// 摄像头
haCamera.setIcon("mdi:cctv");
haCamera.setName("FireBettle2");
// 滑动条1
number1.setIcon("mdi:alpha-x");
number1.setName("X");
number1.setMin(0); // can be float if precision is set via the constructor
number1.setMax(180); // can be float if precision is set via the constructor
number1.setStep(1);// minimum step: 0.001f
number1.setMode(HANumber::ModeSlider);
number1.setCurrentState(90);
number1.onCommand(onNumberCommand);
// 滑动条2
number2.setIcon("mdi:alpha-y");
number2.setName("Y");
number2.setMin(0); // can be float if precision is set via the constructor
number2.setMax(180); // can be float if precision is set via the constructor
number2.setStep(1);// minimum step: 0.001f
number2.setMode(HANumber::ModeSlider);
number2.setCurrentState(90);
number2.onCommand(onNumberCommand);
// 拍照按钮
button.setIcon("mdi:camera-iris");
button.setName("拍照");
button.onCommand(onButtonCommand);
mqtt.begin(BROKER_ADDR, 1883, "ESP_CAM");
setupServo();
}
// 将图像保存到 SD 卡
void writeFile(fs::FS &fs, const char *path, uint8_t *data, size_t len)
{
File file = fs.open(path, FILE_WRITE);
if (!file)
{
return;
}
file.write(data, len);
file.close();
}
// 从 fb 绘制图片到 TFT 屏幕
// xpos, ypos 是左上角位置
void drawSdJpeg(camera_fb_t *fb, int xpos, int ypos)
{
// 使用以下方法初始化解码器
bool decoded = JpegDec.decodeArray(fb->buf, fb->len);
// 解码成功
if (decoded)
{
// 将图片渲染到指定位置
jpegRender(xpos, ypos);
}
else
{
Serial.println("Jpeg file format not supported!");
}
}
// 在 TFT 上绘制 JPEG 图像,如果图像不适合,图像将在右侧/底部被裁剪
void jpegRender(int xpos, int ypos)
{
uint16_t *pImg;
uint16_t mcu_w = JpegDec.MCUWidth;
uint16_t mcu_h = JpegDec.MCUHeight;
uint32_t max_x = JpegDec.width;
uint32_t max_y = JpegDec.height;
bool swapBytes = tft.getSwapBytes();
tft.setSwapBytes(true);
// Jpeg 图像被绘制为一组图块,称为最小编码单元,通常是 16x16 像素块
// 确定右边缘和下边缘图像块的宽度和高度
uint32_t min_w = jpg_min(mcu_w, max_x % mcu_w);
uint32_t min_h = jpg_min(mcu_h, max_y % mcu_h);
// 保存当前图像块大小
uint32_t win_w = mcu_w;
uint32_t win_h = mcu_h;
uint32_t drawTime = millis();
// 保存右侧和底部边缘的坐标,以帮助将图像裁剪为屏幕尺寸
max_x += xpos;
max_y += ypos;
// 从文件中获取数据,解码并显示
while (JpegDec.read())
{ // While there is more data in the file
pImg = JpegDec.pImage; // 解码 MCU(最小编码单元,通常是 8x8 或 16x16 像素块)
// 计算当前MCU左上角坐标
int mcu_x = JpegDec.MCUx * mcu_w + xpos;
int mcu_y = JpegDec.MCUy * mcu_h + ypos;
// 检查右边缘是否需要更改图像块大小
if (mcu_x + mcu_w <= max_x)
win_w = mcu_w;
else
win_w = min_w;
// 检查底部边缘的图像块大小是否需要更改
if (mcu_y + mcu_h <= max_y)
win_h = mcu_h;
else
win_h = min_h;
// 将像素复制到连续块中
if (win_w != mcu_w)
{
uint16_t *cImg;
int p = 0;
cImg = pImg + win_w;
for (int h = 1; h < win_h; h++)
{
p += mcu_w;
for (int w = 0; w < win_w; w++)
{
*cImg = *(pImg + w + p);
cImg++;
}
}
}
// 计算必须绘制多少个像素
uint32_t mcu_pixels = win_w * win_h;
// 仅在适合屏幕的情况下绘制图像 MCU 块
if ((mcu_x + win_w) <= tft.width() && (mcu_y + win_h) <= tft.height())
tft.pushImage(mcu_x, mcu_y, win_w, win_h, pImg);
else if ((mcu_y + win_h) >= tft.height())
// 图像已超出屏幕底部,因此中止解码
JpegDec.abort(); //
}
tft.setSwapBytes(swapBytes);
// showTime(millis() - drawTime);
}
// 打印图片信息
// 在 JpegDec.decodeFile(...) 或 JpegDec.decodeArray(...) 之后调用
void jpegInfo()
{
// Print information extracted from the JPEG file
Serial.println("JPEG image info");
Serial.println("===============");
Serial.print("Width :");
Serial.println(JpegDec.width);
Serial.print("Height :");
Serial.println(JpegDec.height);
Serial.print("Components :");
Serial.println(JpegDec.comps);
Serial.print("MCU / row:");
Serial.println(JpegDec.MCUSPerRow);
Serial.print("MCU / col:");
Serial.println(JpegDec.MCUSPerCol);
Serial.print("Scan type:");
Serial.println(JpegDec.scanType);
Serial.print("MCU width:");
Serial.println(JpegDec.MCUWidth);
Serial.print("MCU height :");
Serial.println(JpegDec.MCUHeight);
Serial.println("===============");
Serial.println("");
}
void showTime(uint32_t msTime)
{
// tft.setCursor(0, 0);
// tft.setTextFont(1);
// tft.setTextSize(2);
// tft.setTextColor(TFT_WHITE, TFT_BLACK);
// tft.print(F(" JPEG drawn in "));
// tft.print(msTime);
// tft.println(F(" ms "));
Serial.print(F(" JPEG drawn in "));
Serial.print(msTime);
Serial.println(F(" ms "));
}
void setupSD()
{
// 初始化 SD 卡使用的SPI总线
sdspi.begin(12, 14, 13, 21);
// 初始化 SD 卡
if (!SD.begin(21, sdspi))
{
Serial.println("SD 卡初始化失败");
return;
}
Serial.println("SD 卡初始化成功");
}
// 设置舵机
void setupServo()
{
ESP32PWM::allocateTimer(0);
servoX.setPeriodHertz(50);
servoX.attach(servoXPin, 500, 2400);
servoY.setPeriodHertz(50);
servoY.attach(servoYPin, 500, 2400);
}
// 滑动条回调,控制舵机转动
void onNumberCommand(HANumeric number, HANumber *sender)
{
if (number.isSet())
{
if (sender == &number1)
{
char num = {0};
number.toStr(num);
servoX.write(atoi(num));
Serial.print("X:");
Serial.println(atoi(num));
}
else if (sender == &number2)
{
char num = {0};
number.toStr(num);
servoY.write(180 - atoi(num));
Serial.print("Y:");
Serial.println(180 - atoi(num));
}
}
sender->setState(number); // report the selected option back to the HA panel
}
// 按钮回调
void onButtonCommand(HAButton *sender)
{
if (sender == &button)
{
takepic = true;
}
}
将ESP32接入HA的方式非常多,比如ESPhome,Tasmota,ESPEasy。本示例采用一个HA专用库文件ArduinoHA;为SD卡单独使用了一个SPI通道,避免与摄像头冲突;使用EERPOM记录图片的数量;添加TFT 屏幕显示,以便观察图像,由于HA需要接受JPEG图像格式,显示时需要对JPEG格式解码,但解码效率不理想,每个480×320的画面解码在800ms左右,画面卡顿严重。
5.HA效果展示HA支持设备自动发现,程序上传后,概览中可以Firebeetle CAM 设备。配置界面,可以简单测试点击拍照可以立刻拍摄一张照片,刷新后显示在窗口,并保存照片在SD卡中;拖动X轴,Y轴滑动条可控制舵机转动,调整摄像头方向。
6.无HA的玩法设备与HA交互的方式是MQTT协议,只需要订阅主题就能脱离HA查看和控制。aha/34851891d36c/myCamera/tCamera主题只需订阅即可aha/34851891d36c/NumberY/cmd_t
aha/34851891d36c/NumberX/cmd_t
aha/34851891d36c/button/cmd_t
其他主题需要订阅和发布,其中的 34851891d36c 是生成的,每个设备不一样。安装安卓端mqtt软件MQTT Dash,并作如下配置。 启动软件后,点击右上角加号,添加名称和mqtt服务器地址,保存。 进入创建的Firebeetle条目,点击右上角加号,创建组件,选择Image,订阅topic
保存后退出,如此添加button用于拍照,slider用于控制舵机角度。 添加完成,连接到mqtt服务器即可查看照片和控制舵机。
页:
[1]