2023-8-31 18:36:05 [显示全部楼层]
1659浏览
查看: 1659|回复: 0

[ESP8266/ESP32] 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/,想要体验的小伙伴自行解决。

FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图1

2.硬件连接
SD卡:注意SD卡模块供电是5V
FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图15
TFT屏幕:
FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图16
舵机:注意舵机需要单独供电
FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图17

3.本例用到ArduinoHA库,TFT_eSPI显示库,JPEGDecoder解码库,ESPServo舵机库
FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图2
TFT_eSPI           链接https://github.com/Bodmer/TFT_eSPI

4.代码部分
  1. #include <Arduino.h>
  2. #include <Wire.h>
  3. #include <SPI.h>
  4. #include <WiFi.h>
  5. #include <ArduinoHA.h>
  6. #include <PubSubClient.h>
  7. #include <pins_arduino.h>
  8. #include <FFat.h>
  9. #include <LittleFS.h>
  10. #include <SPIFFS.h>
  11. #include <SD.h>
  12. #include <EEPROM.h>
  13. #include "esp_camera.h"
  14. #include "DFRobot_AXP313A.h"
  15. #include <ESP32Servo.h>
  16. #include <pins_arduino.h>
  17. #include <JPEGDecoder.h>
  18. #include <TFT_eSPI.h>
  19. #define FIREBEETLE_S3_PSRAM
  20. #include "camera_pins.h"
  21. // mtqq 服务器地址
  22. #define BROKER_ADDR IPAddress(192, 168, 123, 209)
  23. // 推送和保存的时间间隔
  24. #define INTERVAL 20000
  25. DFRobot_AXP313A axp;
  26. WiFiClient client;
  27. HADevice device;
  28. HAMqtt mqtt(client, device);
  29. HACamera haCamera("myCamera");
  30. HAButton button("button");
  31. HANumber number1("NumberX", HANumber::PrecisionP0);
  32. HANumber number2("NumberY", HANumber::PrecisionP0);
  33. SPIClass sdspi;
  34. TFT_eSPI tft = TFT_eSPI();
  35. unsigned long lastPublishAt = 0;
  36. uint32_t pic_cnt;
  37. volatile bool takepic = false;
  38. const int servoXPin = A4, servoYPin = A5;
  39. Servo servoX;
  40. Servo servoY;
  41. void startCameraServer();
  42. void setupCamera();
  43. void setupWiFi(const char *ssid, const char *password);
  44. void publishCameraImage();
  45. void setupHA();
  46. void callback(const char *topic, const uint8_t *payload, uint16_t length);
  47. void writeFile(fs::FS &fs, const char *path, uint8_t *data, size_t len);
  48. void showTime(uint32_t msTime);
  49. void jpegInfo();
  50. void jpegRender(int xpos, int ypos);
  51. void drawSdJpeg(camera_fb_t *fb, int xpos, int ypos);
  52. void showTime(uint32_t msTime);
  53. void setupSD();
  54. void setupServo();
  55. void onNumberCommand(HANumeric number, HANumber *sender);
  56. void onButtonCommand(HAButton *sender);
  57. void setup()
  58. {
  59.     Serial.begin(115200);
  60.     while (!Serial)
  61.         ;
  62.     Serial.setDebugOutput(true);
  63.     // 初始化tft屏幕
  64.     tft.init();
  65.     tft.setRotation(3);
  66.     while (axp.begin() != 0)
  67.     {
  68.         Serial.println("init error");
  69.         delay(1000);
  70.     }
  71.     // 设置摄像头供电
  72.     axp.enableCameraPower(axp.eOV2640);
  73.     // 初始化 Camera
  74.     setupCamera();
  75.     // 连接到wifi
  76.     setupWiFi("ShuangYY", "334452000");
  77.     // 配置HA
  78.     setupHA();
  79.     setupSD();
  80.     EEPROM.begin(4);
  81.     pic_cnt = EEPROM.readUInt(0);
  82. }
  83. void loop()
  84. {
  85.     mqtt.loop();
  86.     // 返回摄像头照片
  87.     camera_fb_t *fb = esp_camera_fb_get();
  88.     // 读取失败
  89.     if (!fb)
  90.     {
  91.         return;
  92.     }
  93.     if (millis() - lastPublishAt > INTERVAL || takepic)
  94.     {
  95.         if (takepic == true)
  96.             takepic = false;
  97.         lastPublishAt = millis();
  98.         publishCameraImage_and_take_photo(pic_cnt, fb);
  99.         pic_cnt++;
  100.         EEPROM.writeUInt(0, pic_cnt);
  101.     }
  102.     // 解码 JEGP 图片
  103.     drawSdJpeg(fb, 0, 0);
  104.     // 释放缓存
  105.     esp_camera_fb_return(fb);
  106. }
  107. // 配置 摄像头
  108. void setupCamera()
  109. {
  110.     camera_config_t config;
  111.     config.ledc_channel = LEDC_CHANNEL_0;
  112.     config.ledc_timer = LEDC_TIMER_0;
  113.     config.pin_d0 = Y2_GPIO_NUM;
  114.     config.pin_d1 = Y3_GPIO_NUM;
  115.     config.pin_d2 = Y4_GPIO_NUM;
  116.     config.pin_d3 = Y5_GPIO_NUM;
  117.     config.pin_d4 = Y6_GPIO_NUM;
  118.     config.pin_d5 = Y7_GPIO_NUM;
  119.     config.pin_d6 = Y8_GPIO_NUM;
  120.     config.pin_d7 = Y9_GPIO_NUM;
  121.     config.pin_xclk = XCLK_GPIO_NUM;
  122.     config.pin_pclk = PCLK_GPIO_NUM;
  123.     config.pin_vsync = VSYNC_GPIO_NUM;
  124.     config.pin_href = HREF_GPIO_NUM;
  125.     config.pin_sccb_sda = SIOD_GPIO_NUM;
  126.     config.pin_sccb_scl = SIOC_GPIO_NUM;
  127.     config.pin_pwdn = PWDN_GPIO_NUM;
  128.     config.pin_reset = RESET_GPIO_NUM;
  129.     config.xclk_freq_hz = 20000000;
  130.     config.frame_size = FRAMESIZE_HVGA;
  131.     config.pixel_format = PIXFORMAT_JPEG; // for streaming
  132.     // config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition
  133.     config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  134.     config.fb_location = CAMERA_FB_IN_PSRAM;
  135.     config.jpeg_quality = 12;
  136.     config.fb_count = 1;
  137.     if (psramFound())
  138.     {
  139.         config.jpeg_quality = 10;
  140.         config.fb_count = 2;
  141.         config.grab_mode = CAMERA_GRAB_LATEST;
  142.         Serial.println("PSRAM 启用成功");
  143.     }
  144.     else
  145.     {
  146.         // Limit the frame size when PSRAM is not available
  147.         config.frame_size = FRAMESIZE_SVGA;
  148.         config.fb_location = CAMERA_FB_IN_DRAM;
  149.         Serial.println("PSRAM 启用失败");
  150.     }
  151.     // camera init
  152.     esp_err_t err = esp_camera_init(&config);
  153.     if (err != ESP_OK)
  154.     {
  155.         Serial.printf("Camera init failed with error 0x%x", err);
  156.         return;
  157.     }
  158. }
  159. // 接入WiFi
  160. void setupWiFi(const char *ssid = "ShuangYY", const char *password = "334452000")
  161. {
  162.     WiFi.begin(ssid, password);
  163.     WiFi.setSleep(false);
  164.     while (WiFi.status() != WL_CONNECTED)
  165.     {
  166.         delay(500);
  167.         Serial.print(".");
  168.     }
  169.     Serial.println("");
  170.     Serial.println("WiFi connected");
  171.     startCameraServer();
  172.     Serial.print("Camera Ready! Use 'http://");
  173.     Serial.print(WiFi.localIP());
  174.     Serial.println("' to connect");
  175. }
  176. // 推送数据到 mqtt 服务器,并保存到 SD 卡
  177. void publishCameraImage_and_take_photo(int n, camera_fb_t *fb)
  178. {
  179.     // 将照片推送到HA
  180.     haCamera.publishImage(fb->buf, fb->len);
  181.     char filename[32];
  182.     sprintf(filename, "/image%d.jpg", n);
  183.     // 将照片写入到SD卡
  184.     writeFile(SD, filename, fb->buf, fb->len);
  185. }
  186. // 配置HA的参数
  187. void setupHA()
  188. {
  189.     byte mac[6];
  190.     WiFi.macAddress(mac);
  191.     device.setUniqueId(mac, sizeof(mac));
  192.     // set device's details (optional)
  193.     device.setName("FireBettle2-CAM");
  194.     device.setSoftwareVersion("1.2.3");
  195.     device.setManufacturer("DIY");
  196.     // 摄像头
  197.     haCamera.setIcon("mdi:cctv");
  198.     haCamera.setName("FireBettle2");
  199.     // 滑动条1
  200.     number1.setIcon("mdi:alpha-x");
  201.     number1.setName("X");
  202.     number1.setMin(0);   // can be float if precision is set via the constructor
  203.     number1.setMax(180); // can be float if precision is set via the constructor
  204.     number1.setStep(1);  // minimum step: 0.001f
  205.     number1.setMode(HANumber::ModeSlider);
  206.     number1.setCurrentState(90);
  207.     number1.onCommand(onNumberCommand);
  208.     // 滑动条2
  209.     number2.setIcon("mdi:alpha-y");
  210.     number2.setName("Y");
  211.     number2.setMin(0);   // can be float if precision is set via the constructor
  212.     number2.setMax(180); // can be float if precision is set via the constructor
  213.     number2.setStep(1);  // minimum step: 0.001f
  214.     number2.setMode(HANumber::ModeSlider);
  215.     number2.setCurrentState(90);
  216.     number2.onCommand(onNumberCommand);
  217.     // 拍照按钮
  218.     button.setIcon("mdi:camera-iris");
  219.     button.setName("拍照");
  220.     button.onCommand(onButtonCommand);
  221.     mqtt.begin(BROKER_ADDR, 1883, "ESP_CAM");
  222.     setupServo();
  223. }
  224. // 将图像保存到 SD 卡
  225. void writeFile(fs::FS &fs, const char *path, uint8_t *data, size_t len)
  226. {
  227.     File file = fs.open(path, FILE_WRITE);
  228.     if (!file)
  229.     {
  230.         return;
  231.     }
  232.     file.write(data, len);
  233.     file.close();
  234. }
  235. // 从 fb 绘制图片到 TFT 屏幕
  236. // xpos, ypos 是左上角位置
  237. void drawSdJpeg(camera_fb_t *fb, int xpos, int ypos)
  238. {
  239.     // 使用以下方法初始化解码器
  240.     bool decoded = JpegDec.decodeArray(fb->buf, fb->len);
  241.     // 解码成功
  242.     if (decoded)
  243.     {
  244.         // 将图片渲染到指定位置
  245.         jpegRender(xpos, ypos);
  246.     }
  247.     else
  248.     {
  249.         Serial.println("Jpeg file format not supported!");
  250.     }
  251. }
  252. // 在 TFT 上绘制 JPEG 图像,如果图像不适合,图像将在右侧/底部被裁剪
  253. void jpegRender(int xpos, int ypos)
  254. {
  255.     uint16_t *pImg;
  256.     uint16_t mcu_w = JpegDec.MCUWidth;
  257.     uint16_t mcu_h = JpegDec.MCUHeight;
  258.     uint32_t max_x = JpegDec.width;
  259.     uint32_t max_y = JpegDec.height;
  260.     bool swapBytes = tft.getSwapBytes();
  261.     tft.setSwapBytes(true);
  262.     // Jpeg 图像被绘制为一组图块,称为最小编码单元,通常是 16x16 像素块
  263.     // 确定右边缘和下边缘图像块的宽度和高度
  264.     uint32_t min_w = jpg_min(mcu_w, max_x % mcu_w);
  265.     uint32_t min_h = jpg_min(mcu_h, max_y % mcu_h);
  266.     // 保存当前图像块大小
  267.     uint32_t win_w = mcu_w;
  268.     uint32_t win_h = mcu_h;
  269.     uint32_t drawTime = millis();
  270.     // 保存右侧和底部边缘的坐标,以帮助将图像裁剪为屏幕尺寸
  271.     max_x += xpos;
  272.     max_y += ypos;
  273.     // 从文件中获取数据,解码并显示
  274.     while (JpegDec.read())
  275.     {                          // While there is more data in the file
  276.         pImg = JpegDec.pImage; // 解码 MCU(最小编码单元,通常是 8x8 或 16x16 像素块)
  277.         // 计算当前MCU左上角坐标
  278.         int mcu_x = JpegDec.MCUx * mcu_w + xpos;
  279.         int mcu_y = JpegDec.MCUy * mcu_h + ypos;
  280.         // 检查右边缘是否需要更改图像块大小
  281.         if (mcu_x + mcu_w <= max_x)
  282.             win_w = mcu_w;
  283.         else
  284.             win_w = min_w;
  285.         // 检查底部边缘的图像块大小是否需要更改
  286.         if (mcu_y + mcu_h <= max_y)
  287.             win_h = mcu_h;
  288.         else
  289.             win_h = min_h;
  290.         // 将像素复制到连续块中
  291.         if (win_w != mcu_w)
  292.         {
  293.             uint16_t *cImg;
  294.             int p = 0;
  295.             cImg = pImg + win_w;
  296.             for (int h = 1; h < win_h; h++)
  297.             {
  298.                 p += mcu_w;
  299.                 for (int w = 0; w < win_w; w++)
  300.                 {
  301.                     *cImg = *(pImg + w + p);
  302.                     cImg++;
  303.                 }
  304.             }
  305.         }
  306.         // 计算必须绘制多少个像素
  307.         uint32_t mcu_pixels = win_w * win_h;
  308.         // 仅在适合屏幕的情况下绘制图像 MCU 块
  309.         if ((mcu_x + win_w) <= tft.width() && (mcu_y + win_h) <= tft.height())
  310.             tft.pushImage(mcu_x, mcu_y, win_w, win_h, pImg);
  311.         else if ((mcu_y + win_h) >= tft.height())
  312.             // 图像已超出屏幕底部,因此中止解码
  313.             JpegDec.abort(); //
  314.     }
  315.     tft.setSwapBytes(swapBytes);
  316.     // showTime(millis() - drawTime);
  317. }
  318. // 打印图片信息
  319. // 在 JpegDec.decodeFile(...) 或 JpegDec.decodeArray(...) 之后调用
  320. void jpegInfo()
  321. {
  322.     // Print information extracted from the JPEG file
  323.     Serial.println("JPEG image info");
  324.     Serial.println("===============");
  325.     Serial.print("Width      :");
  326.     Serial.println(JpegDec.width);
  327.     Serial.print("Height     :");
  328.     Serial.println(JpegDec.height);
  329.     Serial.print("Components :");
  330.     Serial.println(JpegDec.comps);
  331.     Serial.print("MCU / row  :");
  332.     Serial.println(JpegDec.MCUSPerRow);
  333.     Serial.print("MCU / col  :");
  334.     Serial.println(JpegDec.MCUSPerCol);
  335.     Serial.print("Scan type  :");
  336.     Serial.println(JpegDec.scanType);
  337.     Serial.print("MCU width  :");
  338.     Serial.println(JpegDec.MCUWidth);
  339.     Serial.print("MCU height :");
  340.     Serial.println(JpegDec.MCUHeight);
  341.     Serial.println("===============");
  342.     Serial.println("");
  343. }
  344. void showTime(uint32_t msTime)
  345. {
  346.     // tft.setCursor(0, 0);
  347.     // tft.setTextFont(1);
  348.     // tft.setTextSize(2);
  349.     // tft.setTextColor(TFT_WHITE, TFT_BLACK);
  350.     // tft.print(F(" JPEG drawn in "));
  351.     // tft.print(msTime);
  352.     // tft.println(F(" ms "));
  353.     Serial.print(F(" JPEG drawn in "));
  354.     Serial.print(msTime);
  355.     Serial.println(F(" ms "));
  356. }
  357. void setupSD()
  358. {
  359.     // 初始化 SD 卡使用的SPI总线
  360.     sdspi.begin(12, 14, 13, 21);
  361.     // 初始化 SD 卡
  362.     if (!SD.begin(21, sdspi))
  363.     {
  364.         Serial.println("SD 卡初始化失败");
  365.         return;
  366.     }
  367.     Serial.println("SD 卡初始化成功");
  368. }
  369. // 设置舵机
  370. void setupServo()
  371. {
  372.     ESP32PWM::allocateTimer(0);
  373.     servoX.setPeriodHertz(50);
  374.     servoX.attach(servoXPin, 500, 2400);
  375.     servoY.setPeriodHertz(50);
  376.     servoY.attach(servoYPin, 500, 2400);
  377. }
  378. // 滑动条回调,控制舵机转动
  379. void onNumberCommand(HANumeric number, HANumber *sender)
  380. {
  381.     if (number.isSet())
  382.     {
  383.         if (sender == &number1)
  384.         {
  385.             char num[10] = {0};
  386.             number.toStr(num);
  387.             servoX.write(atoi(num));
  388.             Serial.print("X:");
  389.             Serial.println(atoi(num));
  390.         }
  391.         else if (sender == &number2)
  392.         {
  393.             char num[10] = {0};
  394.             number.toStr(num);
  395.             servoY.write(180 - atoi(num));
  396.             Serial.print("Y:");
  397.             Serial.println(180 - atoi(num));
  398.         }
  399.     }
  400.     sender->setState(number); // report the selected option back to the HA panel
  401. }
  402. // 按钮回调
  403. void onButtonCommand(HAButton *sender)
  404. {
  405.     if (sender == &button)
  406.     {
  407.         takepic = true;
  408.     }
  409. }
复制代码
将ESP32接入HA的方式非常多,比如ESPhome,Tasmota,ESPEasy。本示例采用一个HA专用库文件ArduinoHA;为SD卡单独使用了一个SPI通道,避免与摄像头冲突;使用EERPOM记录图片的数量;添加TFT 屏幕显示,以便观察图像,由于HA需要接受JPEG图像格式,显示时需要对JPEG格式解码,但解码效率不理想,每个480×320的画面解码在800ms左右,画面卡顿严重。

5.HA效果展示
FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图4
HA支持设备自动发现,程序上传后,概览中可以Firebeetle CAM 设备。
FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图3
配置界面,可以简单测试
FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图5
点击拍照可以立刻拍摄一张照片,刷新后显示在窗口,并保存照片在SD卡中;拖动X轴,Y轴滑动条可控制舵机转动,调整摄像头方向。

6.无HA的玩法
设备与HA交互的方式是MQTT协议,只需要订阅主题就能脱离HA查看和控制。
FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图6
  1. aha/34851891d36c/myCamera/t
复制代码
Camera主题只需订阅即可
FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图7
  1. aha/34851891d36c/NumberY/cmd_t
  2. aha/34851891d36c/NumberX/cmd_t
  3. aha/34851891d36c/button/cmd_t
复制代码
其他主题需要订阅和发布,其中的 34851891d36c 是生成的,每个设备不一样。
安装安卓端mqtt软件MQTT Dash,并作如下配置。
FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图8         FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图9
启动软件后,点击右上角加号,添加名称和mqtt服务器地址,保存。
FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图10         FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图11
进入创建的Firebeetle条目,点击右上角加号,创建组件选择Image,订阅topic

         FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图12
保存后退出,如此添加button用于拍照,slider用于控制舵机角度。
FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图13         FireBeetle 2 ESP32-S3   自制监控并接入HomeAssistant图14
添加完成,连接到mqtt服务器即可查看照片和控制舵机。

13.jpg
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail