项目介绍和创意介绍
大家好, 很荣幸能够参加到本次的M-design活动, 我这次参加的任务是:方向三(无线通信、物联网 )。因为本次在贸泽商城里下单的这块乐鑫的ESP32-C6-dev-kit很适合做一些物联网的应用。接下来我对项目进行以下简单的介绍, 项目主要采用了乐鑫的ESP32-C6-dev-kit作主控, 通过I2C协议读取湿温度传感器,光照传感器等. 通过MQTT协议将数据上传给部署在香橙派Zero3上的MQTT服务器. 同时由同时在香橙派Zero3 上的Homeassistant 进行对MQTT指定Topic的监视,使其可以使用HomeAssistant的手机APP或者网站对数据进行可视化. 同时香橙派Zero3上部署Nginx服务器用于OTA升级.
简短的使用到的硬件介绍
硬件介绍
- ESP3-C6-dev-kit开发板
ESP32-C6-DevKitC-1 是一款入门级开发板,使用带有 8 MB SPI flash 的通用型模组 ESP32-C6-WROOM-1(U)。该款开发板具备完整的 Wi-Fi、低功耗蓝牙、Zigbee 及 Thread 功能。
- 香橙派Zero3
Orange Pi Zero 3是一款基于ARM架构的单板计算机,搭载了全志H618系统级芯片,具备高性能的Cortex-A53四核处理器,工作频率可达1.5GHz。 这款开发板支持1GB、1.5GB、2GB或4GB的LPDDR4内存,为用户提供了灵活的配置选择。 - AHT10
AHT10是一款高精度,完全校准,贴片封装的温湿度传感器,MEMS的制作工艺,确保产品具有极高的可靠性与卓越的长期稳定性 - BH1750
BH1750( GY-302 )是一种数字型光强度传感器集成电路,特别适用于两线式串行总线接口。 这种传感器能够根据收集的光线强度数据来调整液晶或者键盘背景灯的亮度,而且利用其高分辨率可以探测较大范围的光强度变化。 - 0.92 寸 Oled屏幕
屏幕采用I2C通讯, 可以和上述的传感器共用一根I2C总线
软件介绍
- Home Assistant
Home Assistant(简称HA)是一个开源的智能家居平台,允许不同品牌、不同协议和不同标准的智能家居设备接入,并通过它进行控制或实现自动化。 - Nginx
Nginx是异步框架的网页服务器,也可以用作反向代理、负载平衡器和HTTP缓存。在本项目中用作静态资源的反向代理. - 开发框架ESP-IDF
ESP-IDF 是乐鑫为ESP 系列芯片提供的物联网开发框架: ESP-IDF 包含一系列库及头文件,提供了基于ESP SoC 构建软件项目所需的核心组件; - Home Assistant APP
Home Assistant APP 是 Home Assistant 提供的移动端应用程序, 支持IOS和安卓用户. 可以便携的读取或者配置传感器.
方案框图和项目设计思路介绍
项目的主要设计思路是由于ESP32-C6-dev-kit这块开发板具有WIFI和蓝牙功能,适合来进行无线传输数据. 通过将采集到的数据通过MQTT服务进行数据传输给香橙派Zero3,然后由其搭载的HomeAssistant服务对MQTT进行数据进行监视.然后将数据的结果显示在手机端的Home Assistant上从而实现了数据的可视化. 而ESP32-C6-devkit搭载的8MBFlash也可以为本次OTA固件的升级提供可能(作为数据采集终端). 同时香橙派部署的Nginx服务可以为OTA固件提供静态代理,使其OTA升级成为可能.
软件流程图和关键代码介绍
当前的系统是根据ESP-IDF的OTA应用程序创建, 所以程序创建起始即具备OTA功能. 对与用户需要做的便是提供一个可以被访问固件的代理地址. 所以我们需要在香橙派上安装Nginx服务,然后将固件上传至香橙派的Nginx代理目录.从而使其ESP32C6开发板上电的时候可以从Nginx代理的地址中来下载固件信息,从而进行OTA升级(如下图所示)
void simple_ota_example_task(void *pvParameter)
{
ESP_LOGI(TAG, "Starting OTA example task");
esp_http_client_config_t config = {
.url = CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL,
#ifdef CONFIG_EXAMPLE_USE_CERT_BUNDLE
.crt_bundle_attach = esp_crt_bundle_attach,
#else
.cert_pem = (char *)server_cert_pem_start,
#endif /* CONFIG_EXAMPLE_USE_CERT_BUNDLE */
.event_handler = _http_event_handler,
.keep_alive_enable = true,
};
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
ESP_LOGI(TAG, "Attempting to download update from %s", config.url);
esp_err_t ret = esp_https_ota(&ota_config);
if (ret == ESP_OK)
{
ESP_LOGI(TAG, "OTA Succeed, Rebooting...");
esp_restart();
}
else
{
ESP_LOGE(TAG, "Firmware upgrade failed");
}
while (1)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
上述代码为OTA升级的核心代码, 即开发板启动的时候从配置好的固件地址根据版本号尝试下载新的固件. 其URL地址在menuconfig中进行配置, 如下图所示.
之后便是需要对MQTT的连接数据进行初始化,使其可以连接到香橙派Zero3上的MQTT服务(实际上这里使用第三方的MQTT服务也是可以的,比如说阿里云的MQTT服务)
void mqtt_init()
{
const esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.hostname = "192.168.1.113",
.broker.address.port = 1883,
.broker.address.transport = MQTT_TRANSPORT_OVER_TCP,
.credentials.client_id = "esp32c6",
.credentials.username = "yiwen",
.credentials.authentication.password = "yiwenxxxx"};
client = esp_mqtt_client_init(&mqtt_cfg);
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, client);
esp_mqtt_client_start(client);
}
上述代码主要是完成MQTT的初始化方法, 需要注意的是在初始化的时候注册了一个MQTT的事件回调函数, 其维护了MQTT的连接的各个状态, 比如说连接成功、订阅成功、接触订阅、断开连接、收到消息等. 如果需要做MQTT消息回掉的控制,可以在这里进行变更(本次程序没有用到)
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%" PRIi32 "", base, event_id);
esp_mqtt_event_handle_t event = event_data;
esp_mqtt_client_handle_t client = event->client;
int msg_id;
switch ((esp_mqtt_event_id_t)event_id)
{
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "MQTT_EVENT_DATA");
// 打印接收到的主题和数据
printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
printf("DATA=%.*s\r\n", event->data_len, event->data);
break;
case MQTT_EVENT_ERROR:
ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT)
{
log_error_if_nonzero("reported from esp-tls", event->error_handle->esp_tls_last_esp_err);
log_error_if_nonzero("reported from tls stack", event->error_handle->esp_tls_stack_err);
log_error_if_nonzero("captured as transport's socket errno", event->error_handle->esp_transport_sock_errno);
ESP_LOGI(TAG, "Last errno string (%s)", strerror(event->error_handle->esp_transport_sock_errno));
}
break;
default:
ESP_LOGI(TAG, "Other event id:%d", event->event_id);
break;
}
}
之后便是初始化三个任务用于控制Oled屏幕显示、AHT10传感器数据读取、和bh1750的光照强度读取.
xTaskCreate(&task_display, "task_display", 8192, NULL, 5, NULL);
xTaskCreate(&aht10_task, "aht10_test", 8192, NULL, 5, NULL);
xTaskCreate(&bh1750_task, "bh1750_test", 8192, NULL, 5, NULL);
其中, SSD1306的库函数是从GITHUB上找到的ESP-IDF的驱动库, 因此驱动OLED并没有耗费我太多的功夫. 但是由于其他的驱动库函数比如说AHT10 或者 BH1750其提供的驱动库函数是基于老版本的 i2c.h 库, 而不是i2c_master库, 所以两者并不能兼容使用. 因此我自己手动的根据手册写了AHT10 和BH1750的驱动.
BH1750驱动
#include "bh1750.h"
#include "driver/i2c.h"
#include <stdio.h>
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c.h"
#define BH1750_ADDR 0x23
#define BH1750_CMD_START 0x10
static const char *TAG = "BH1750";
extern QueueHandle_t light_intensity_handler;
extern i2c_master_bus_handle_t i2c_bus_handle;
i2c_master_dev_handle_t bh1750_dev_handle;
esp_err_t bh1750_init()
{
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = BH1750_ADDR,
.scl_speed_hz = 400000,
};
esp_err_t ret = i2c_master_bus_add_device(i2c_bus_handle, &dev_cfg, &bh1750_dev_handle);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to create I2C device handle: %d", ret);
return ret;
}
ESP_LOGI(TAG, "Initializing BH1750...");
uint8_t start_cmd = BH1750_CMD_START; // 启动命令
ret = i2c_master_transmit(bh1750_dev_handle, &start_cmd, 1, 1000 / portTICK_PERIOD_MS);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to send start command: %d", ret);
return ret;
}
vTaskDelay(pdMS_TO_TICKS(10)); // 等待传感器响应
ESP_LOGI(TAG, "BH1750 initialized successfully.");
return ESP_OK;
}
esp_err_t bh1750_read_light_intensity(float *light_intensity)
{
uint8_t data[2];
esp_err_t ret = i2c_master_receive(bh1750_dev_handle, data, sizeof(data), 1000 / portTICK_PERIOD_MS);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to read data: %d", ret);
return ret;
}
uint16_t raw_light = (data[0] << 8) | data[1];
*light_intensity = raw_light / 1.2; // 转换为光强度
return ESP_OK;
}
void bh1750_task(void *pvParameters)
{
if (bh1750_init() != ESP_OK)
{
ESP_LOGE(TAG, "Failed to initialize BH1750");
return;
}
float light_intensity;
while (1)
{
if (bh1750_read_light_intensity(&light_intensity) == ESP_OK)
{
ESP_LOGI(TAG, "Light Intensity: %.2f lx", light_intensity);
xQueueSend(light_intensity_handler, &light_intensity, 0);
}
else
{
ESP_LOGE(TAG, "Failed to read data from BH1750");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
AHT10 驱动
#include "aht10.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c.h"
#define AHT10_I2C_ADDRESS 0x38
#define AHT10_CMD_INIT 0xE1
#define AHT10_CMD_TRIGGER 0xAC
#define AHT10_CMD_SOFT_RESET 0xBA
static const char *TAG = "AHT10";
extern QueueHandle_t temperature_handler;
extern QueueHandle_t humidity_handler;
extern i2c_master_bus_handle_t i2c_bus_handle;
i2c_master_dev_handle_t aht10_dev_handle;
esp_err_t aht10_init()
{
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = AHT10_I2C_ADDRESS,
.scl_speed_hz = 400000,
};
esp_err_t ret = i2c_master_bus_add_device(i2c_bus_handle, &dev_cfg, &aht10_dev_handle);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to create I2C device handle: %d", ret);
return ret;
}
ESP_LOGI(TAG, "Initializing AHT10...");
uint8_t init_cmd[3] = {AHT10_CMD_INIT, 0x08, 0x00}; // 初始化命令
ret = i2c_master_transmit(aht10_dev_handle, init_cmd, sizeof(init_cmd), 1000 / portTICK_PERIOD_MS);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to send init command: %d", ret);
return ret;
}
vTaskDelay(pdMS_TO_TICKS(10)); // 等待传感器响应
// 读取状态寄存器
uint8_t status;
ret = i2c_master_receive(aht10_dev_handle, &status, 1, 1000 / portTICK_PERIOD_MS);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to read status register: %d", ret);
return ret;
}
if (status & 0x08)
{
ESP_LOGI(TAG, "AHT10 initialized successfully.");
return ESP_OK;
}
else
{
ESP_LOGE(TAG, "AHT10 initialization failed, status: 0x%02X", status);
return ESP_FAIL;
}
}
esp_err_t aht10_read_temperature_and_humidity(float *temperature, float *humidity)
{
uint8_t trigger_cmd = AHT10_CMD_TRIGGER;
esp_err_t ret = i2c_master_transmit(aht10_dev_handle, &trigger_cmd, 1, 1000 / portTICK_PERIOD_MS);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to send trigger command: %d", ret);
return ret;
}
vTaskDelay(pdMS_TO_TICKS(80)); // 等待测量完成
uint8_t data[6];
ret = i2c_master_receive(aht10_dev_handle, data, sizeof(data), 1000 / portTICK_PERIOD_MS);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to read data: %d", ret);
return ret;
}
uint32_t raw_humidity = ((uint32_t)data[1] << 12) | ((uint32_t)data[2] << 4) | (data[3] >> 4);
uint32_t raw_temperature = ((uint32_t)data[3] & 0x0F) << 16 | ((uint32_t)data[4] << 8) | data[5];
*humidity = (raw_humidity * 100.0) / 1048576.0;
*temperature = (raw_temperature * 200.0) / 1048576.0 - 50.0;
return ESP_OK;
}
void aht10_task(void *pvParameters)
{
if (aht10_init() != ESP_OK)
{
ESP_LOGE(TAG, "Failed to initialize AHT10");
return;
}
float temperature, humidity;
while (1)
{
if (aht10_read_temperature_and_humidity(&temperature, &humidity) == ESP_OK)
{
ESP_LOGI(TAG, "Temp: %.2f C, Humidity: %.2f %%", temperature, humidity);
xQueueSend(temperature_handler, &temperature, 0);
xQueueSend(humidity_handler, &humidity, 0);
}
else
{
ESP_LOGE(TAG, "Failed to read data from AHT10");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
可以在上述两个驱动函数里看到, 我在读取到各项传感器的数据之后将其发送到了对应的消息队列. 主要的数据处理其实是在屏幕显示的部分.
void task_display(void *pvParameters)
{
char text[16];
char light_text[16];
while (1)
{
float temperature = 0;
float humidity = 0;
float light_intensity = 0;
if (xQueueReceive(temperature_handler, &temperature, 0) == pdTRUE)
{
ESP_LOGI(tag, "Temperature: %.2f", temperature);
// 发送温度数据到MQTT主题
char temperature_message[64];
snprintf(temperature_message, sizeof(temperature_message), "{\"temperature\": %.2f}", temperature);
esp_mqtt_client_publish(client, "bedroom/temperature", temperature_message, 0, 1, 0);
}
if (xQueueReceive(humidity_handler, &humidity, 0) == pdTRUE)
{
ESP_LOGI(tag, "Humidity: %.2f", humidity);
// 发送湿度数据到MQTT主题
char humidity_message[64];
snprintf(humidity_message, sizeof(humidity_message), "{\"humidity\": %.2f}", humidity);
esp_mqtt_client_publish(client, "bedroom/humidity", humidity_message, 0, 1, 0);
}
if (xQueueReceive(light_intensity_handler, &light_intensity, 0) == pdTRUE)
{
ESP_LOGI(tag, "Light Intensity: %.2f", light_intensity);
// 发送光照强度数据到MQTT主题
char light_intensity_message[64];
snprintf(light_intensity_message, sizeof(light_intensity_message), "{\"illuminance\": %.2f}", light_intensity);
esp_mqtt_client_publish(client, "bedroom/light", light_intensity_message, 0, 1, 0);
}
snprintf(text, sizeof(text), "T:%.2f H:%.2f", temperature, humidity);
snprintf(light_text, sizeof(light_text), "L:%.2f", light_intensity);
ssd1306_display_text(&dev, 0, text, strlen(text), false);
ssd1306_display_text(&dev, 1, light_text, strlen(light_text), false);
ssd1306_show_buffer(&dev);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
当屏幕显示的时候,首先从消息队列中取出来已经保存的数据,然后将数据发送到MQTT服务器里对应的主题上,接着调用屏幕驱动函数对屏幕进行点亮. 从而实现了屏幕的显示功能. 后续的MQTT数据则是发送到了MQTT服务器.如下所示为实时监视到的数据信息.
而通过在docker 容器的home assistant 内部的configuration.yml的配置则使其可以监视到对应的传感器的主题信息,从而可以将对应的传感器识别为一个实体显示在home assistant的web端或者是移动端.
至此数据的完成交互完毕!
功能展示图及说明
总体展示
当开发版采集到数据的时候, 首先会将数据在Oled显示屏上进行显示, 之后数据会被同步到MQTT服务器上, 可以从上述图片中清晰的观察到, 其OLED湿度、温度、 和光照信息,均和手机端保持一致!
下图为遮盖光照传感器后的效果
手机端历史记录的查看
当用户点击对应的传感器时,即可查看到对应传感器数据变化的历史记录!
Web端
上图为Home Assistant 端实时监听到的传感器信息, 分别是湿度信息、 光照信息和温度信息.
历史数据的查看
当鼠标点击任何一个传感器的时候即可查看到对应传感器的历史数据信息.
手机端数据的显示
上图为传感器总览
上图为历史记录
设计中遇到的难题和解决方法
这次项目中其实遇见了很多麻烦的事情, 比如说香橙派烧录系统之后无法联网和更新软件源,导致无法安装docker 和 home assistant 以及Nginx等. 在经过多个软件镜像的重试和查阅多个资料后最终是完成了软件的安装. 还有一点就是在调试ESP-IDF对AHT10, BH1750和SSD1306的驱动的时候. 由于ESP-IDF组件使用的库不一致导致同一根I2C总线的驱动不能共用,从而导致报错. 最终只能自己花了好大的功夫重新把AHT10的驱动重新完成了一份. 因为是使用的同一根I2C总线,所以BH1750的逻辑可以大概参考AHT10的,因此还节省了一定的时间.最终完成了任务!
对本次竞赛的心得体会
我对本次最大的体会就是我学习和收获到了如何使用MQTT对物联网设备进行数据的收集和处理. 同时学习到了ESP-IDF框架中I2C部分的使用. 我在这里非常感谢电子森林和贸泽电子提供的参赛机会. 这次参赛的条件非常宽松,所以用户可以发挥自己的IDEA来结合现有的设备快速的来将自己的创意进行实现. 这样的活动真的非常好!