DFSkccpIWF8 发表于 2023-8-31 18:36:05

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]
查看完整版本: FireBeetle 2 ESP32-S3 自制监控并接入HomeAssistant