| 
 本例实现将Firebeetle CAM 接入到HomeAssistant中,并通过舵机控制摄像头角度,定时或按需求获取摄像头画面,并将照片保存在SD卡中。  
1.HA简介 HomeAssistant简称HA,是用于家庭自动化的免费开源软件,旨在成为智能家居设备的中央控制系统,是控制物联网由模块化集成组件支持的连接技术设备、软件、应用程序和服务,包括用于蓝牙、Zigbee和Z-Wave等无线通信协议的本机集成组件,通关专用API或MQTT等方式提供公共访问,以便通过局域网或互联网进行第三方集成,互联互通各家智能设备,如小米,涂鸦,YeeLight等,更是支持DIY智能硬件接入,联动整个智能生态。  
 
2.硬件连接 SD卡:注意SD卡模块供电是5V TFT屏幕: 舵机:注意舵机需要单独供电  
3.本例用到 ArduinoHA库,TFT_eSPI显示库,JPEGDecoder解码库,ESPServo舵机库  
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[32];
 -     sprintf(filename, "/image%d.jpg", n);
 -     // 将照片写入到SD卡
 -     writeFile(SD, filename, fb->buf, fb->len);
 - }
 - // 配置HA的参数
 - void setupHA()
 - {
 -     byte mac[6];
 -     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[10] = {0};
 -             number.toStr(num);
 -             servoX.write(atoi(num));
 -             Serial.print("X:");
 -             Serial.println(atoi(num));
 -         }
 -         else if (sender == &number2)
 -         {
 -             char num[10] = {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/t
 
  复制代码
 Camera主题只需订阅即可 - 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服务器即可查看照片和控制舵机。  
 |