From a0febb1e5bd76d9e0e879ce77f72c6c8a71160b9 Mon Sep 17 00:00:00 2001 From: Wang Beihong Date: Sat, 7 Feb 2026 23:04:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=99=BA=E8=83=BD=E5=AE=B6=E5=B1=85?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E7=B3=BB=E7=BB=9F=20v1.0=20=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 环境监测:温湿度/光照/空气质量传感器采集 - 智能控制:时间段/降温/通风三种自动模式 - 闹钟系统:3个闹钟+温和唤醒功能 - 远程控制:MQTT双向通信 - 本地显示:LVGL图形界面 - 双MCU架构,FreeRTOS 10任务并行 - 完整的1250行README文档 --- .clangd | 2 + .gitignore | 78 + .vscode/c_cpp_properties.json | 19 + .vscode/launch.json | 10 + .vscode/settings.json | 5 + CMakeLists.txt | 6 + README.md | 1250 +++++++ components/lvgl_st7735s_use/CMakeLists.txt | 4 + .../include/lvgl_st7735s_use.h | 33 + .../lvgl_st7735s_use/include/ui_display.h | 38 + .../lvgl_st7735s_use/lvgl_st7735s_use.c | 236 ++ components/lvgl_st7735s_use/ui_display.c | 258 ++ .../protocol_examples_common/CMakeLists.txt | 52 + .../CMakeLists.txt:Zone.Identifier | Bin 0 -> 25 bytes .../Kconfig.projbuild | 504 +++ .../Kconfig.projbuild:Zone.Identifier | Bin 0 -> 25 bytes components/protocol_examples_common/README.md | 66 + .../README.md:Zone.Identifier | Bin 0 -> 25 bytes .../addr_from_stdin.c | 69 + .../addr_from_stdin.c:Zone.Identifier | Bin 0 -> 25 bytes components/protocol_examples_common/connect.c | 148 + .../connect.c:Zone.Identifier | Bin 0 -> 25 bytes .../protocol_examples_common/console_cmd.c | 87 + .../console_cmd.c:Zone.Identifier | Bin 0 -> 25 bytes .../protocol_examples_common/eth_connect.c | 245 ++ .../eth_connect.c:Zone.Identifier | Bin 0 -> 25 bytes .../include/addr_from_stdin.h | 44 + .../include/addr_from_stdin.h:Zone.Identifier | Bin 0 -> 25 bytes .../include/example_common_private.h | 58 + .../example_common_private.h:Zone.Identifier | Bin 0 -> 25 bytes .../include/protocol_examples_common.h | 154 + ...protocol_examples_common.h:Zone.Identifier | Bin 0 -> 25 bytes .../include/protocol_examples_thread_config.h | 115 + ...l_examples_thread_config.h:Zone.Identifier | Bin 0 -> 25 bytes .../include/protocol_examples_utils.h | 49 + .../protocol_examples_utils.h:Zone.Identifier | Bin 0 -> 25 bytes .../protocol_examples_common/ppp_connect.c | 260 ++ .../ppp_connect.c:Zone.Identifier | Bin 0 -> 25 bytes .../protocol_examples_utils.c | 388 ++ .../protocol_examples_utils.c:Zone.Identifier | Bin 0 -> 25 bytes .../protocol_examples_common/stdin_out.c | 32 + .../stdin_out.c:Zone.Identifier | Bin 0 -> 25 bytes .../protocol_examples_common/thread_connect.c | 130 + .../thread_connect.c:Zone.Identifier | Bin 0 -> 25 bytes .../protocol_examples_common/wifi_connect.c | 247 ++ .../wifi_connect.c:Zone.Identifier | Bin 0 -> 25 bytes components/serial_mcu/CMakeLists.txt | 4 + components/serial_mcu/include/serial_mcu.h | 2 + components/serial_mcu/serial_mcu.c | 47 + dependencies.lock | 108 + main/CMakeLists.txt | 6 + main/idf_component.yml | 31 + main/main.c | 3127 +++++++++++++++++ partitions.csv | 6 + 54 files changed, 7918 insertions(+) create mode 100644 .clangd create mode 100644 .gitignore create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 components/lvgl_st7735s_use/CMakeLists.txt create mode 100644 components/lvgl_st7735s_use/include/lvgl_st7735s_use.h create mode 100644 components/lvgl_st7735s_use/include/ui_display.h create mode 100644 components/lvgl_st7735s_use/lvgl_st7735s_use.c create mode 100644 components/lvgl_st7735s_use/ui_display.c create mode 100644 components/protocol_examples_common/CMakeLists.txt create mode 100644 components/protocol_examples_common/CMakeLists.txt:Zone.Identifier create mode 100644 components/protocol_examples_common/Kconfig.projbuild create mode 100644 components/protocol_examples_common/Kconfig.projbuild:Zone.Identifier create mode 100644 components/protocol_examples_common/README.md create mode 100644 components/protocol_examples_common/README.md:Zone.Identifier create mode 100644 components/protocol_examples_common/addr_from_stdin.c create mode 100644 components/protocol_examples_common/addr_from_stdin.c:Zone.Identifier create mode 100644 components/protocol_examples_common/connect.c create mode 100644 components/protocol_examples_common/connect.c:Zone.Identifier create mode 100644 components/protocol_examples_common/console_cmd.c create mode 100644 components/protocol_examples_common/console_cmd.c:Zone.Identifier create mode 100644 components/protocol_examples_common/eth_connect.c create mode 100644 components/protocol_examples_common/eth_connect.c:Zone.Identifier create mode 100644 components/protocol_examples_common/include/addr_from_stdin.h create mode 100644 components/protocol_examples_common/include/addr_from_stdin.h:Zone.Identifier create mode 100644 components/protocol_examples_common/include/example_common_private.h create mode 100644 components/protocol_examples_common/include/example_common_private.h:Zone.Identifier create mode 100644 components/protocol_examples_common/include/protocol_examples_common.h create mode 100644 components/protocol_examples_common/include/protocol_examples_common.h:Zone.Identifier create mode 100644 components/protocol_examples_common/include/protocol_examples_thread_config.h create mode 100644 components/protocol_examples_common/include/protocol_examples_thread_config.h:Zone.Identifier create mode 100644 components/protocol_examples_common/include/protocol_examples_utils.h create mode 100644 components/protocol_examples_common/include/protocol_examples_utils.h:Zone.Identifier create mode 100644 components/protocol_examples_common/ppp_connect.c create mode 100644 components/protocol_examples_common/ppp_connect.c:Zone.Identifier create mode 100644 components/protocol_examples_common/protocol_examples_utils.c create mode 100644 components/protocol_examples_common/protocol_examples_utils.c:Zone.Identifier create mode 100644 components/protocol_examples_common/stdin_out.c create mode 100644 components/protocol_examples_common/stdin_out.c:Zone.Identifier create mode 100644 components/protocol_examples_common/thread_connect.c create mode 100644 components/protocol_examples_common/thread_connect.c:Zone.Identifier create mode 100644 components/protocol_examples_common/wifi_connect.c create mode 100644 components/protocol_examples_common/wifi_connect.c:Zone.Identifier create mode 100644 components/serial_mcu/CMakeLists.txt create mode 100644 components/serial_mcu/include/serial_mcu.h create mode 100644 components/serial_mcu/serial_mcu.c create mode 100644 dependencies.lock create mode 100644 main/CMakeLists.txt create mode 100644 main/idf_component.yml create mode 100644 main/main.c create mode 100644 partitions.csv diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..437f255 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Remove: [-f*, -m*] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7805557 --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Directory metadata +.directory + +# Temporary files +*~ +*.swp +*.swo +*.bak +*.tmp + +# Log files +*.log + +# Build artifacts and directories +**/build/ +build/ +*.o +*.a +*.out +*.exe # For any host-side utilities compiled on Windows + +# ESP-IDF specific build outputs +*.bin +*.elf +*.map +flasher_args.json # Generated in build directory +sdkconfig.old +sdkconfig + +# ESP-IDF dependencies +# For older versions or manual component management +/components/.idf/ +**/components/.idf/ +# For modern ESP-IDF component manager +managed_components/ +# If ESP-IDF tools are installed/referenced locally to the project +.espressif/ + +# CMake generated files +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +install_manifest.txt +CTestTestfile.cmake + +# Python environment files +*.pyc +*.pyo +*.pyd +__pycache__/ +*.egg-info/ +dist/ + +# Virtual environment folders +venv/ +.venv/ +env/ + +# Language Servers +.clangd/ +.ccls-cache/ +compile_commands.json + +# Windows specific +Thumbs.db +ehthumbs.db +Desktop.ini + +# User-specific configuration files +*.user +*.workspace # General workspace files, can be from various tools +*.suo # Visual Studio Solution User Options +*.sln.docstates # Visual Studio diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..cf1970a --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,19 @@ +{ + "configurations": [ + { + "name": "ESP-IDF", + "compilerPath": "/home/beihong/.espressif/tools/riscv32-esp-elf/esp-14.2.0_20251107/riscv32-esp-elf/bin/riscv32-esp-elf-gcc", + "compileCommands": "${config:idf.buildPath}/compile_commands.json", + "includePath": [ + "${workspaceFolder}/**" + ], + "browse": { + "path": [ + "${workspaceFolder}" + ], + "limitSymbolsToIncludedHeaders": true + } + } + ], + "version": 4 +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3694ae4 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,10 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "gdbtarget", + "request": "attach", + "name": "Eclipse CDT GDB Adapter" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..76c6d6c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "idf.currentSetup": "/home/beihong/esp/v5.5.2/esp-idf", + "idf.port": "/dev/ttyACM0", + "idf.flashType": "UART" +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..200706d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(iot-home) diff --git a/README.md b/README.md new file mode 100644 index 0000000..a082b7c --- /dev/null +++ b/README.md @@ -0,0 +1,1250 @@ +# IoT-Home 智能家居控制系统 + +[![ESP-IDF](https://img.shields.io/badge/ESP--IDF-5.5.2-blue.svg)](https://github.com/espressif/esp-idf) +[![ESP32-C3](https://img.shields.io/badge/Chip-ESP32--C3-green.svg)](https://www.espressif.com/zh-hans/products/socs/esp32-c3) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Version](https://img.shields.io/badge/version-1.0-orange.svg)](VERSION) + +一个基于 ESP32-C3 的完整智能家居控制系统,集成了环境传感器采集、智能自动控制、远程MQTT通信、本地可视化显示等功能。 + +--- + +## 目录 + +- [项目简介](#项目简介) +- [硬件清单](#硬件清单) +- [系统架构](#系统架构) +- [功能特性](#功能特性) +- [快速开始](#快速开始) +- [配置说明](#配置说明) +- [API文档](#api文档) +- [故障排查](#故障排查) +- [技术栈](#技术栈) + +--- + +## 项目简介 + +### 基本信息 + +- **项目名称**: IoT-Home 智能家居控制系统 +- **目标芯片**: ESP32-C3 (RISC-V 单核 160MHz) +- **开发框架**: ESP-IDF 5.5.2 +- **项目类型**: 智能家居控制系统 +- **作者**: beihong.wang +- **版本**: 1.0 +- **创建日期**: 2026-01-19 + +### 功能概述 + +本项目是一个功能完善的智能家居控制系统,主要特性包括: + +1. **环境监测**: 集成温湿度、光照强度、空气质量三种传感器 +2. **智能控制**: 根据时间段和环境数据自动控制窗帘、照明、风扇 +3. **闹钟系统**: 支持多闹钟,具备温和唤醒功能(提前开窗帘) +4. **远程控制**: 基于MQTT协议的云端双向通信 +5. **本地显示**: LVGL图形界面,实时显示环境数据 +6. **双MCU架构**: ESP32主控 + 副MCU扩展控制能力 +7. **持久化配置**: NVS存储所有配置参数 + +--- + +## 硬件清单 + +### 主控芯片 + +| 参数 | 说明 | +|------|------| +| 芯片型号 | ESP32-C3 | +| 架构 | RISC-V 32位 | +| 运行频率 | 160 MHz | +| Flash | 4 MB | +| SRAM | 400 KB | +| 无线模块 | Wi-Fi 2.4GHz + BLE 5.0 | + +### 传感器模块 + +| 传感器型号 | 功能 | 接口 | GPIO | 说明 | +|-----------|------|------|------|------| +| **AHT30** | 温湿度传感器 | I2C | SCL=5, SDA=4 | 温度-40~85°C,湿度0~100% | +| **BH1750** | 光照强度传感器 | I2C | SCL=5, SDA=4 | 测量范围1~65535 lux | +| **MQ135** | 空气质量传感器 | ADC | ADC1_CH0 (GPIO34) | 有害气体浓度检测 | + +### 执行器件 + +| 设备 | 功能 | 接口 | GPIO | 控制方式 | +|------|------|------|------|---------| +| **SG90舵机** | 窗帘开合控制 | PWM | GPIO10 | 30°=关, 195°=开 | +| **LED灯** | 照明控制 | 串口 | UART1_TX | 亮度0-100% | +| **风扇** | 通风降温 | GPIO | GPIO1 | 高低电平控制 | +| **蜂鸣器** | 声音提示 | 串口 | UART1_TX | 开关控制 | +| **LCD屏幕** | 本地显示 | SPI | GPIO2,3,6,7,8,9 | ST7735S 160x80 | + +### 通信接口 + +| 接口 | 用途 | 配置 | +|------|------|------| +| Wi-Fi | 网络通信 | 2.4GHz, WPA2/WPA3 | +| UART1 | 副MCU通信 | 230400 baud, TX=GPIO21, RX=GPIO20 | +| I2C | 传感器总线 | 100kHz, SCL=GPIO5, SDA=GPIO4 | +| SPI2 | LCD显示 | 40MHz, SPI2_HOST | + +--- + +## 系统架构 + +### 整体架构图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 应用层 │ +├──────────────┬──────────────┬──────────────┬───────────────┤ +│ 传感器采集 │ 自动控制 │ 设备控制 │ 通信上报 │ +│ │ │ │ │ +│ - AHT30 │ - 时间段控制 │ - 窗帘控制 │ - MQTT发布 │ +│ - BH1750 │ - 降温模式 │ - LED控制 │ - MQTT订阅 │ +│ - MQ135 │ - 通风模式 │ - 风扇控制 │ - 命令解析 │ +│ │ │ - 蜂鸣器控制 │ │ +├──────────────┴──────────────┴──────────────┴───────────────┤ +│ FreeRTOS 任务层 (10个任务) │ +├─────────────────────────────────────────────────────────────────┤ +│ ESP-IDF 组件层 │ +├──────────┬──────────┬──────────┬──────────┬──────────────┤ +│ I2C驱动 │ SPI驱动 │ UART驱动 │ MQTT组件 │ NVS组件 │ +├──────────┴──────────┴──────────┴──────────┴──────────────┤ +│ 硬件抽象层 (HAL) │ +├─────────────────────────────────────────────────────────────────┤ +│ ESP32-C3 硬件层 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 任务架构 + +系统使用 FreeRTOS 多任务架构,10个任务协同工作: + +| 任务名称 | 优先级 | 栈大小 | 周期 | 功能描述 | +|---------|--------|--------|------|---------| +| `i2c0_ahtxx_task` | 5 | 4096 | 5秒 | AHT30温湿度采集 | +| `i2c0_bh1750_task` | 5 | 4096 | 5秒 | BH1750光照采集 | +| `mq135_task` | 5 | 4096 | 5秒 | MQ135空气质量采集 | +| `mqtt_publish_task` | 5 | 4096 | 3/50秒 | MQTT数据发布 | +| `peripheral_control_task` | 5 | 4096 | 100ms | 外设控制执行 | +| `alarm_clock_main_task` | 5 | 8192 | 1秒 | 闹钟时间检查 | +| `time_period_task` | 5 | 4096 | 10秒 | 时间段智能控制 | +| `cooling_mode_task` | 5 | 4096 | 5秒 | 降温模式监控 | +| `ventilation_mode_task` | 5 | 4096 | 5秒 | 通风模式监控 | +| `uart_rx_task` | 24 | 4096 | 事件驱动 | UART按键接收 | + +### 同步机制 + +为防止数据竞争,系统使用4个互斥锁保护共享资源: + +| 互斥锁 | 保护对象 | 用途 | +|--------|---------|------| +| `xSensorDataMutex` | `g_sensor_data` | 保护传感器数据读写 | +| `xMqttMessageMutex` | `g_device_message` | 保护MQTT消息构建 | +| `xTimePeriodMutex` | `g_day_period`, `g_night_period` | 保护时间段配置 | +| `xControlFlagMutex` | 所有控制标志 | 保护设备控制状态 | + +--- + +## 功能特性 + +### 1. 环境监测 + +#### AHT30 温湿度传感器 + +- **采集周期**: 5秒 +- **温度范围**: -40°C ~ 85°C (精度±0.3°C) +- **湿度范围**: 0% ~ 100% (精度±2%) +- **错误处理**: 初始化失败时自动重试,标记数据无效 + +#### BH1750 光照传感器 + +- **采集周期**: 5秒 +- **测量模式**: 连续测量,1lux分辨率 +- **数据范围**: 1 ~ 65535 lux +- **光强状态判断**: + - `BRIGHT` (光照充足): > 500 lux + - `MODERATE` (光照适中): 200-500 lux + - `DIM` (光照不足): < 200 lux + +#### MQ135 空气质量传感器 + +- **采集方式**: ADC1 12位采样 +- **测量原理**: Rs/R0 = 116.602 × ppm^(-2.769) +- **动态校准**: 启动后50次采样平均计算R0值 +- **数据范围**: 1.0 ~ 1000.0 ppm +- **空气质量等级**: + | 范围 | 等级 | 颜色 | + |------|------|------| + | ≤ 20 ppm | 优秀 (Excellent) | 绿色 | + | 21-100 ppm | 良好 (Good) | 黑色 | + | 101-300 ppm | 轻度污染 (Moderate) | 橙色 | + | > 300 ppm | 中重度污染 (High) | 红色 | + +### 2. 智能自动控制 + +#### 时间段智能控制 + +系统根据当前时间段和环境光强自动控制窗帘和照明: + +**白天模式** (默认 6:00-18:00): +- **光照充足** (>500 lux): 关窗帘 + 关灯 +- **光照不足** (<200 lux): 开窗帘 + 开灯(亮度80%) +- **光照适中** (200-500 lux): 开窗帘(利用自然光) + 关灯 + +**晚上模式** (默认 18:00-6:00): +- 自动关闭窗帘 +- 开启照明(亮度50%) + +**特性**: +- NVS持久化时间段配置 +- 支持跨天时间段配置(如18:00-6:00) +- 10秒检查周期,快速响应光强变化 +- 手动控制检测,避免冲突 + +#### 降温模式 + +根据室内温度自动控制风扇: + +- **温度阈值**: 默认28°C (可通过MQTT配置,范围10-40°C) +- **控制逻辑**: + - 温度 > 阈值: 自动开启风扇 + - 温度 < (阈值-1°C): 自动关闭风扇 (1°C滞后防止抖动) +- **高温提醒**: + - 温度 > 35°C: 触发蜂鸣器 + MQTT推送 "温度过高请注意通风" + - 温度恢复正常: 自动关闭提醒 +- **配置**: NVS持久化温度阈值 + +#### 自动通风模式 + +根据空气质量自动通风: + +- **空气质量阈值**: 50 ppm +- **控制逻辑**: + - 空气质量 > 50 ppm: 开启风扇 + 蜂鸣器短促提示(200ms) + - 空气质量 < 40 ppm: 关闭风扇 (10ppm滞后) +- **提醒机制**: MQTT推送 "卧室需要通风" (仅推送一次) + +### 3. 闹钟系统 + +#### 多闹钟支持 + +- **闹钟数量**: 3个独立闹钟 +- **时间格式**: HH:MM:SS +- **触发机制**: ESP定时器每秒检查时间 +- **持续时间**: 蜂鸣器响5秒后自动关闭 + +#### 温和唤醒功能 + +**闹钟1特有功能**: 提前3分钟自动打开窗帘,让自然光温和唤醒用户 + +- 前提: 闹钟已启用 +- 触发时间: 闹钟时间前3分钟 +- 动作: 舵机转到开窗帘位置(195°) +- 标志: 设置`curtain_opened`标志防止重复 + +#### 配置管理 + +- NVS自动保存闹钟时间和开关状态 +- 支持MQTT远程设置闹钟 +- 支持独立启用/禁用每个闹钟 + +### 4. 设备控制 + +#### 窗帘控制 + +- **执行器件**: SG90舵机 +- **控制方式**: LEDC定时器0, 50Hz PWM +- **角度范围**: 30°(关) ~ 195°(开) +- **校准**: 支持自定义校准值 + +#### LED照明控制 + +- **控制方式**: UART1发送给副MCU +- **亮度范围**: 0-100% +- **控制协议**: + ``` + 帧头: 0x55 + 命令: 0x01 (LED控制) + 数据: 亮度值 (0-100) + ``` + +#### 风扇控制 + +- **控制方式**: GPIO1直接高低电平 +- **状态**: 开/关 +- **应用**: 降温模式、通风模式、手动控制 + +#### 蜂鸣器控制 + +- **控制方式**: UART1发送给副MCU +- **控制协议**: + ``` + 帧头: 0x55 + 命令: 0x02 (蜂鸣器控制) + 数据: 0(关)/1(开) + ``` +- **应用场景**: 闹钟、高温提醒、空气质量提醒 + +#### 屏幕背光控制 + +- **控制方式**: GPIO6直接高低电平 +- **特性**: 根据在家状态自动控制 + +### 5. MQTT通信 + +#### 连接配置 + +- **Broker**: mqtt://beihong.wang:1883 +- **认证**: 用户名/密码认证 +- **客户端ID**: esp32_+MAC后3位 +- **QoS**: 发布QoS0, 订阅QoS2 + +#### 主题设计 + +| 方向 | 主题 | 说明 | +|------|------|------| +| 发布 | `topic/sensor/esp32_iothome_001` | 传感器数据上报 | +| 订阅 | `topic/control/esp32_iothome_001` | 控制命令接收 | + +#### 上报频率 + +- **在家模式**: 3秒周期 +- **离家模式**: 50秒周期 + +### 6. 本地显示 + +#### LVGL图形界面 + +- **LCD**: ST7735S, 160x80分辨率 +- **颜色格式**: RGB565 (16位色) +- **刷新率**: 约30FPS (33ms) +- **特性**: 双缓冲,提高流畅度 + +#### 界面页面 + +**时间页面** (默认): +- 日期和星期 +- 时间 (HH:MM:SS) +- 大字体显示 + +**传感器页面**: +- 温度 (保留2位小数) +- 湿度 (保留2位小数) +- 光照强度 (保留2位小数) +- 空气质量指数 + 颜色分级 + +#### 页面切换 + +- **触发**: UART按键6 (0xAA 0x06 0x55) +- **显示**: 默认时间页面,按键切换到传感器页面 + +### 7. MCU间通信 + +#### UART协议 + +- **波特率**: 230400 +- **数据位**: 8 +- **停止位**: 1 +- **校验**: 无 + +#### 接收协议 (副MCU → ESP32) + +- **格式**: AA [01-06] 55 (3字节) +- **按键映射**: + | 按键 | 命令 | 功能 | + |------|------|------| + | 按键1 | 0x01 | LED开关 | + | 按键2 | 0x02 | 风扇开关 | + | 按键3 | 0x03 | 窗帘开关 | + | 按键4 | 0x04 | 蜂鸣器开关 | + | 按键5 | 0x05 | 屏幕背光 | + | 按键6 | 0x06 | 页面切换 | + +### 8. 时间同步 + +#### SNTP配置 + +- **时区**: 北京时间 (CST-8) +- **NTP服务器**: + - cn.pool.ntp.org + - ntp1.aliyun.com +- **协议**: NTP v4 (UDP 123端口) +- **自动同步**: 启动后自动同步 + +--- + +## 快速开始 + +### 环境准备 + +#### 安装ESP-IDF + +```bash +# 克隆ESP-IDF仓库 +git clone --recursive https://github.com/espressif/esp-idf.git +cd esp-idf + +# 切换到v5.5.2版本 +git checkout v5.5.2 +git submodule update --init --recursive + +# 安装依赖 +./install.sh esp32c3 + +# 激活环境 +source ./export.sh +``` + +#### 依赖组件 + +本项目使用以下managed components,会自动下载: +- `espp__wifi`: Wi-Fi连接示例组件 +- LVGL相关组件: 图形界面库 + +### 编译与烧录 + +#### 1. 编译项目 + +```bash +cd /home/beihong/esp_projects/iot-home +idf.py build +``` + +#### 2. 烧录到设备 + +```bash +idf.py flash +``` + +#### 3. 监控串口输出 + +```bash +idf.py monitor +``` + +#### 4. 一键烧录+监控 + +```bash +idf.py flash monitor +``` + +### 首次启动流程 + +#### 系统上电启动详细流程 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ ESP32-C3 上电启动 │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 1. 系统初始化 (app_main) │ +│ - 设置日志级别 │ +│ - 初始化NVS(非易失性存储) │ +│ - 创建事件循环 │ +│ - 初始化ESP-NETIF │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 2. Wi-Fi连接 │ +│ - 配置Wi-Fi参数 (SSID/Password) │ +│ - 扫描并连接到AP │ +│ - 最多重试6次 │ +│ - 打印AP信息 (MAC, RSSI, 信道) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 3. 硬件外设初始化 │ +│ - I2C总线初始化 (100kHz, SCL=GPIO5, SDA=GPIO4) │ +│ - 舵机初始化 (GPIO10, LEDC定时器0, 50Hz) │ +│ - GPIO输出初始化 (风扇GPIO1) │ +│ - UART1初始化 (230400 baud, TX=GPIO21, RX=GPIO20) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 4. LVGL图形界面初始化 │ +│ - SPI总线配置 (40MHz, SPI2_HOST) │ +│ - LCD驱动初始化 (ST7735S, 160x80) │ +│ - LVGL库初始化 │ +│ - 创建UI组件 (时间页面、传感器页面) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 5. 串口通信初始化 │ +│ - serial_mcu_init() │ +│ - 配置UART1接收中断 │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 6. MQTT客户端启动 │ +│ - 配置MQTT连接参数 │ +│ - 启动MQTT客户端 │ +│ - 等待连接建立 │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 7. 互斥锁创建 │ +│ - xSensorDataMutex (传感器数据保护) │ +│ - xMqttMessageMutex (MQTT消息保护) │ +│ - xTimePeriodMutex (时间段配置保护) │ +│ - xControlFlagMutex (控制标志保护) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 8. NVS配置加载 │ +│ - 从NVS加载降温模式配置 (温度阈值) │ +│ - 从NVS加载时间段配置 (白天/晚上模式) │ +│ - 从NVS加载闹钟配置 (3个闹钟时间和状态) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 9. 创建FreeRTOS任务 │ +│ 按5级优先级依次创建以下10个任务: │ +│ │ +│ 优先级24 (最高): │ +│ ├─ uart_rx_task [4096字节] 事件驱动 │ +│ │ +│ 优先级5: │ +│ ├─ i2c0_ahtxx_task [4096字节] 5秒周期 │ +│ ├─ i2c0_bh1750_task [4096字节] 5秒周期 │ +│ ├─ mq135_task [4096字节] 5秒周期 (预热期短) │ +│ ├─ mqtt_publish_task [4096字节] 3/50秒周期 │ +│ ├─ peripheral_control_task [4096字节] 100ms周期 │ +│ ├─ alarm_clock_main_task [8192字节] 1秒周期 │ +│ ├─ time_period_task [4096字节] 10秒周期 │ +│ ├─ cooling_mode_task [4096字节] 5秒周期 │ +│ └─ ventilation_mode_task [4096字节] 5秒周期 │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 10. 主循环启动 │ +│ 主任务进入while(1)循环,定期打印传感器数据 │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ 系统运行状态 │ + │ (所有任务并行) │ + └─────────────────┘ + │ + ┌───────────────────┼───────────────────┐ + ▼ ▼ ▼ + ┌─────────┐ ┌──────────┐ ┌──────────┐ + │传感器采集 │ │ 自动控制 │ │MQTT通信 │ + │ 任务组 │ │ 任务组 │ │ 任务组 │ + └─────────┘ └──────────┘ └──────────┘ +``` + +#### 各任务启动后执行流程 + +**1. AHT30温湿度采集任务 (i2c0_ahtxx_task)** +``` +启动 → 初始化I2C → 等待AHT30就绪 → 读取温湿度数据 + ↓ (每5秒) +更新g_sensor_data → 更新LVGL显示 → 互斥锁保护 → 循环 +``` + +**2. BH1750光照采集任务 (i2c0_bh1750_task)** +``` +启动 → 初始化I2C → 配置连续测量模式 → 读取光照数据 + ↓ (每5秒) +更新g_sensor_data → 更新LVGL显示 → 判断光强状态 → 循环 +``` + +**3. MQ135空气质量采集任务 (mq135_task)** +``` +启动 → 初始化ADC → 预热阶段(100次采样,约8分钟) + ↓ +校准阶段(100次采样,计算R0) → 温湿度补偿 → 滑动平均滤波 + ↓ (每5秒) +计算Rs/R0 → 应用MQ135公式 → 异常检测 → 更新数据 → 循环 +``` + +**4. MQTT发布任务 (mqtt_publish_task)** +``` +启动 → 等待MQTT连接 → 检查在家/离家状态 + ↓ +在家模式: 每3秒发布传感器数据 +离家模式: 每50秒发布传感器数据 + ↓ +构建JSON消息 → 互斥锁保护 → 发布到Broker → 循环 +``` + +**5. 外设控制任务 (peripheral_control_task)** +``` +启动 → 读取当前控制标志 → 检测状态变化 + ↓ (每100ms) +状态变化 → 执行硬件控制 (舵机/风扇/蜂鸣器/LED) + ↓ +更新遥测数据 → 上报状态 → 记录上次状态 → 循环 +``` + +**6. 闹钟主任务 (alarm_clock_main_task)** +``` +启动 → 初始化SNTP → 从NVS加载3个闹钟配置 + ↓ +创建定时器 (每1秒检查) → 等待定时器回调 + ↓ (每1秒) +检查当前时间 → 对比闹钟时间 + ├─ 匹配闹钟1: 提前3分钟开窗帘 → 时间到触发蜂鸣器 + ├─ 匹配闹钟2: 直接触发蜂鸣器 + └─ 匹配闹钟3: 直接触发蜂鸣器 + ↓ +更新闹钟触发状态 → 循环 +``` + +**7. 时间段智能控制任务 (time_period_task)** +``` +启动 → 获取当前时间 → 判断白天/晚上模式 + ↓ (每10秒) +读取光照强度 → 判断光强状态 (充足/适中/不足) + ↓ +根据模式和光强决定控制策略 + ├─ 白天+光照充足: 关窗帘+关灯 + ├─ 白天+光照不足: 开窗帘+开灯(80%) + ├─ 白天+光照适中: 开窗帘+关灯 + └─ 晚上: 关窗帘+开灯(50%) + ↓ +检查手动控制冲突 → 互斥锁保护 → 执行控制 → 上报状态 → 循环 +``` + +**8. 降温模式任务 (cooling_mode_task)** +``` +启动 → 从NVS加载温度阈值 → 初始化高温提醒标志 + ↓ (每5秒) +读取当前温度 → 与阈值比较 + ├─ 温度 > 阈值: 开启风扇 + ├─ 温度 < (阈值-1°C): 关闭风扇 (滞后防抖) + └─ 温度 > 35°C: 触发高温提醒 (蜂鸣器+MQTT推送) + ↓ +互斥锁保护 → 执行控制 → 更新标志 → 循环 +``` + +**9. 通风模式任务 (ventilation_mode_task)** +``` +启动 → 初始化空气质量阈值 → 初始化提醒标志 + ↓ (每5秒) +读取当前空气质量 → 与阈值比较 + ├─ 质量 > 50ppm: 开启风扇 + 蜂鸣器短促提示 + │ 发送MQTT提醒 "卧室需要通风" + └─ 质量 < 40ppm: 关闭风扇 (滞后防抖) + 重置提醒标志 + ↓ +互斥锁保护 → 执行控制 → 更新标志 → 循环 +``` + +**10. UART接收任务 (uart_rx_task)** +``` +启动 → 等待副MCU按键数据 + ↓ (事件驱动) +接收完整帧 (AA cmd 55) + ├─ 0x01: LED开关 → 翻转light_source_control_flag + ├─ 0x02: 风扇开关 → 翻转fan_control_flag + ├─ 0x03: 窗帘开关 → 翻转servo_control_flag + ├─ 0x04: 蜂鸣器开关 → 翻转buzzer_control_flag + ├─ 0x05: 背光开关 → 翻转led_backlight_on + └─ 0x06: 页面切换 → 调用ui_toggle_page() + ↓ +互斥锁保护 → 更新状态 → 上报MQTT → 继续等待 +``` + +#### MQTT事件处理流程 + +``` +MQTT事件回调 + │ + ├─ MQTT_EVENT_CONNECTED: + │ └─ 订阅控制主题 → 准备接收远程命令 + │ + ├─ MQTT_EVENT_DATA: + │ └─ 接收到控制命令JSON + │ │ + │ ├─ 解析设备ID和类型 (验证是否匹配) + │ ├─ 解析message_type + │ │ + │ └─ 根据controls执行操作: + │ ├─ fan_state: open/close → 设置风扇 + │ ├─ curtain_state: open/close → 设置窗帘 + │ ├─ led_state: open/close + led_power → 设置LED + │ ├─ buzzer_state: open/close → 设置蜂鸣器 + │ ├─ alarm1_time/enable → 配置闹钟1 + │ ├─ alarm2_time/enable → 配置闹钟2 + │ ├─ alarm3_time/enable → 配置闹钟3 + │ ├─ day_period_start/end → 配置白天时间段 + │ ├─ night_period_start/end → 配置晚上时间段 + │ └─ temperature_threshold → 配置降温阈值 + │ + └─ MQTT_EVENT_DISCONNECTED: + └─ 等待自动重连 +``` + +#### 系统状态同步流程 + +``` +本地操作 (按键/自动控制) + │ + ↓ +更新控制标志 (互斥锁保护) + │ + ├─ → 外设控制任务检测到变化 + │ ↓ + │ 执行硬件控制 (GPIO/UART/PWM) + │ ↓ + │ 更新遥测数据 + │ + └─ → MQTT发布任务周期性上报 + ↓ + 发送传感器数据到云端 + ↓ + 远程端(小程序/服务器)接收状态 + │ + ↓ + 用户查看/控制 + │ + ↓ + 发送MQTT控制命令 + │ + ↓ + ESP32接收并执行 + │ + └─ 循环 +``` + +#### 传感器数据流向 + +``` +┌──────────┐ +│ AHT30 │ → 温度/湿度 → g_sensor_data → LVGL显示 + MQTT上报 +└──────────┘ + +┌──────────┐ +│ BH1750 │ → 光照强度 → g_sensor_data → LVGL显示 + 时间段控制 +└──────────┘ + +┌──────────┐ +│ MQ135 │ → 空气质量 → g_sensor_data → LVGL显示 + 通风控制 +│ (预热期)│ → 校准R0 → 温湿度补偿 → 滑动平均滤波 +└──────────┘ +``` + +#### 数据存储与恢复 + +**配置保存 (NVS存储):** +- 降温模式温度阈值 (cooling_mode_namespace) +- 时间段配置 (day_period, night_period) +- 闹钟配置 (3个闹钟的时间和开关状态) + +**配置加载:** +- 启动时自动从NVS读取 +- MQTT可远程修改并自动保存 +- 断电后配置自动恢复 + +--- + +## 配置说明 + +### Wi-Fi配置 + +编辑 `sdkconfig`: + +```ini +CONFIG_EXAMPLE_WIFI_SSID="Your_WiFi_SSID" +CONFIG_EXAMPLE_WIFI_PASSWORD="Your_WiFi_Password" +CONFIG_EXAMPLE_WIFI_CONN_MAX_RETRY=6 +``` + +### MQTT配置 + +编辑 `main/main.c`: + +```c +// MQTT Broker地址 +#define MQTT_BROKER_URL "mqtt://your-broker.com:1883" + +// 认证信息 +#define MQTT_USERNAME "your_username" +#define MQTT_PASSWORD "your_password" + +// 设备标识 +#define MQTT_CLIENT_ID "esp32_your_device_id" + +// 主题配置 +#define MQTT_PUBLISH_TOPIC_QOS0 "topic/sensor/esp32_your_device_id" +#define MQTT_NOTIFY_TOPIC "topic/control/esp32_your_device_id" +``` + +### GPIO引脚分配 + +```c +// I2C总线 (传感器) +I2C_SCL: GPIO5 +I2C_SDA: GPIO4 + +// SPI总线 (LCD屏幕) +SPI_CLK: GPIO2 +SPI_MOSI: GPIO3 +LCD_CS: GPIO7 +LCD_DC: GPIO8 +LCD_RST: GPIO9 +LCD_BL: GPIO6 + +// PWM (舵机) +SERVO_PWM: GPIO10 + +// GPIO控制 +FAN_CONTROL: GPIO1 + +// ADC (空气质量传感器) +MQ135_ADC: ADC1_CH0 (GPIO34) + +// UART1 (副MCU通信) +UART_TX: GPIO21 +UART_RX: GPIO20 +``` + +### LVGL显示配置 + +```c +// LCD分辨率 +#define EXAMPLE_LCD_H_RES 160 +#define EXAMPLE_LCD_V_RES 80 + +// SPI配置 +#define EXAMPLE_LCD_SPI_NUM SPI2_HOST +#define EXAMPLE_LCD_PIXEL_CLK_HZ (40 * 1000 * 1000) // 40MHz +#define EXAMPLE_LCD_BITS_PER_PIXEL 16 + +// 绘图缓冲 +#define EXAMPLE_LCD_DRAW_BUFF_HEIGHT 50 +``` + +### 自动控制阈值 + +编辑 `main/main.c`: + +```c +// 降温模式 +float g_temperature_threshold = 28.0f; // 温度阈值 +const float HIGH_TEMP_THRESHOLD = 35.0f; // 高温提醒阈值 + +// 通风模式 +#define AIR_QUALITY_THRESHOLD 50.0f // 空气质量阈值 + +// 光照判断阈值 +#define LIGHT_BRIGHT_THRESHOLD 500.0f // 光照充足阈值 +#define LIGHT_DIM_THRESHOLD 200.0f // 光照不足阈值 +``` + +### 时间段默认配置 + +```c +// 白天模式 (默认6:00-18:00) +time_period_config_t g_day_period = { + .start_hour = 6, + .start_minute = 0, + .end_hour = 18, + .end_minute = 0, + .enabled = true +}; + +// 晚上模式 (默认18:00-6:00) +time_period_config_t g_night_period = { + .start_hour = 18, + .start_minute = 0, + .end_hour = 6, + .end_minute = 0, + .enabled = true +}; +``` + +--- + +## API文档 + +### MQTT消息格式 + +#### 传感器数据上报 + +**发布主题**: `topic/sensor/esp32_iothome_001` + +```json +{ + "type": "device_message", + "device_id": "esp32_bedroom_001", + "device_type": "bedroom_controller", + "timestamp": 1701234567890, + "message_type": "sensor_data", + "data": { + "state": { + "online": true, + "current_mode": "day_mode", + "standby_mode": false, + "error_code": 0 + }, + "telemetry": { + "temperature": 25.50, + "humidity": 60.30, + "light_intensity": 350.00, + "air_quality": 45.00, + "curtain_state": "open", + "led_state": "close", + "led_power": 0, + "fan_state": "close", + "buzzer_state": "close" + } + } +} +``` + +#### 控制命令接收 + +**订阅主题**: `topic/control/esp32_iothome_001` + +```json +{ + "type": "control_command", + "device_id": "esp32_bedroom_001", + "device_type": "bedroom_controller", + "message_type": "device_control", + "data": { + "controls": { + "fan_state": "open|close", + "curtain_state": "open|close", + "led_state": "open|close", + "led_power": 0-100, + "buzzer_state": "open|close", + "alarm1_time": "HH:MM:SS", + "alarm1_enable": "on|off", + "alarm2_time": "HH:MM:SS", + "alarm2_enable": "on|off", + "alarm3_time": "HH:MM:SS", + "alarm3_enable": "on|off", + "day_period_start": "HH:MM", + "day_period_end": "HH:MM", + "night_period_start": "HH:MM", + "night_period_end": "HH:MM", + "temperature_threshold": 28.0 + } + } +} +``` + +### MQTT命令示例 + +#### 控制风扇 + +```bash +mosquitto_pub -h beihong.wang -t topic/control/esp32_iothome_001 -m \ +'{ + "type": "control_command", + "device_id": "esp32_bedroom_001", + "device_type": "bedroom_controller", + "message_type": "device_control", + "data": { + "controls": { + "fan_state": "open" + } + } +}' +``` + +#### 控制窗帘 + +```bash +mosquitto_pub -h beihong.wang -t topic/control/esp32_iothome_001 -m \ +'{ + "type": "control_command", + "device_id": "esp32_bedroom_001", + "device_type": "bedroom_controller", + "message_type": "device_control", + "data": { + "controls": { + "curtain_state": "close" + } + } +}' +``` + +#### 设置LED亮度 + +```bash +mosquitto_pub -h beihong.wang -t topic/control/esp32_iothome_001 -m \ +'{ + "type": "control_command", + "device_id": "esp32_bedroom_001", + "device_type": "bedroom_controller", + "message_type": "device_control", + "data": { + "controls": { + "led_state": "open", + "led_power": 80 + } + } +}' +``` + +#### 设置闹钟 + +```bash +mosquitto_pub -h beihong.wang -t topic/control/esp32_iothome_001 -m \ +'{ + "type": "control_command", + "device_id": "esp32_bedroom_001", + "device_type": "bedroom_controller", + "message_type": "device_control", + "data": { + "controls": { + "alarm1_time": "07:30:00", + "alarm1_enable": "on" + } + } +}' +``` + +#### 设置降温阈值 + +```bash +mosquitto_pub -h beihong.wang -t topic/control/esp32_iothome_001 -m \ +'{ + "type": "control_command", + "device_id": "esp32_bedroom_001", + "device_type": "bedroom_controller", + "message_type": "device_control", + "data": { + "controls": { + "temperature_threshold": 26.0 + } + } +}' +``` + +#### 设置时间段 + +```bash +mosquitto_pub -h beihong.wang -t topic/control/esp32_iothome_001 -m \ +'{ + "type": "control_command", + "device_id": "esp32_bedroom_001", + "device_type": "bedroom_controller", + "message_type": "device_control", + "data": { + "controls": { + "day_period_start": "07:00", + "day_period_end": "19:00", + "night_period_start": "19:00", + "night_period_end": "07:00" + } + } +}' +``` + +### 本地按键控制 + +通过副MCU的物理按键控制: + +| 按键 | 功能 | 说明 | +|------|------|------| +| 按键1 | LED开关 | 切换照明灯开关 | +| 按键2 | 风扇开关 | 切换风扇开关 | +| 按键3 | 窗帘开关 | 切换窗帘开/合 | +| 按键4 | 蜂鸣器开关 | 切换蜂鸣器开关 | +| 按键5 | 背光开关 | 切换屏幕背光 | +| 按键6 | 页面切换 | 切换时间/传感器页面 | + +### 监听传感器数据 + +```bash +mosquitto_sub -h beihong.wang -t topic/sensor/esp32_iothome_001 +``` + +--- + +## 故障排查 + +### 传感器初始化失败 + +**现象**: 日志显示 "AHTxx初始化失败" 或 "BH1750初始化失败" + +**解决方案**: +1. 检查I2C接线 (SCL=GPIO5, SDA=GPIO4) +2. 确认I2C总线上有上拉电阻 (通常4.7kΩ) +3. 检查I2C地址冲突 (AHT30: 0x38, BH1750: 0x23) +4. 确认传感器供电 (3.3V) +5. 使用I2C扫描工具检测设备 + +### MQTT连接失败 + +**现象**: 日志显示 "MQTT_EVENT_ERROR" + +**解决方案**: +1. 检查Wi-Fi连接状态 +2. 验证Broker地址和端口 (mqtt://beihong.wang:1883) +3. 检查用户名密码是否正确 +4. 确认防火墙允许1883端口 +5. 使用 `mosquitto_sub` 测试Broker连接 + +### 显示异常 + +**现象**: 屏幕无显示或花屏 + +**解决方案**: +1. 检查SPI接线 (CLK, MOSI, CS, DC, RST, BL) +2. 验证LCD供电 (3.3V或5V,根据模块规格) +3. 检查背光控制GPIO6连接 +4. 确认LCD型号为ST7735S +5. 检查SPI时钟频率 (40MHz可能过高,尝试降低到20MHz) + +### 舵机不工作 + +**现象**: 窗帘无法开合 + +**解决方案**: +1. 检查GPIO10连接 +2. 确认舵机供电 (需要5V, ESP32的3.3V可能驱动不足) +3. 检查PWM配置 (50Hz, 脉宽1-2ms) +4. 校准角度范围 (30°-195°) +5. 确认舵机型号为SG90或兼容型号 + +### 风扇不工作 + +**现象**: 风扇无法开启 + +**解决方案**: +1. 检查GPIO1连接 +2. 确认风扇供电 (通常5V或12V) +3. 检查风扇驱动电路 (可能需要晶体管驱动) +4. 使用万用表测试GPIO1电平变化 +5. 确认风扇控制逻辑 (高电平开/低电平关) + +### MQ135读数异常 + +**现象**: 空气质量读数过大或过小 + +**解决方案**: +1. 确认传感器预热时间 (MQ135需要预热24-48小时) +2. 检查ADC连接 (ADC1_CH0 / GPIO34) +3. 确认负载电阻RL=10kΩ连接正确 +4. 在干净空气中重启设备进行R0校准 +5. 调整初始R0值 (代码中的R0变量) + +### 时间同步失败 + +**现象**: 时间显示不正确 + +**解决方案**: +1. 检查网络连接 +2. 确认NTP服务器可访问 (cn.pool.ntp.org) +3. 检查防火墙是否允许UDP 123端口 +4. 查看日志中的SNTP同步结果 +5. 手动设置时区代码 (当前为CST-8) + +### NVS存储失败 + +**现象**: 配置无法保存或启动时丢失 + +**解决方案**: +1. 检查分区表是否包含NVS分区 +2. 使用 `idf.py erase-flash` 擦除Flash后重新烧录 +3. 检查NVS分区大小 (至少需要4KB) +4. 查看日志中的NVS初始化信息 +5. 使用 `nvs_partition_generator` 工具重新生成NVS分区 + +--- + +## 技术栈 + +| 类别 | 技术栈 | 说明 | +|------|--------|------| +| **芯片平台** | ESP32-C3 | RISC-V架构,160MHz | +| **操作系统** | FreeRTOS | 实时操作系统 | +| **开发框架** | ESP-IDF 5.5.2 | 乐鑫官方开发框架 | +| **通信协议** | MQTT 3.1.1 | 物联网通信协议 | +| **网络协议** | TCP/IP (LwIP) | 轻量级TCP/IP协议栈 | +| **无线协议** | Wi-Fi 802.11 b/g/n | 2.4GHz无线网络 | +| **时间同步** | SNTP (NTP v4) | 网络时间协议 | +| **图形库** | LVGL 9.4.0 | 轻量级图形库 | +| **数据格式** | JSON (cJSON库) | 数据交换格式 | +| **存储** | NVS | 非易失性存储 | +| **编译器** | GCC (RISC-V) | RISC-V交叉编译器 | +| **构建系统** | CMake + Ninja | 跨平台构建系统 | + +--- + +## 项目结构 + +``` +iot-home/ +├── main/ # 主程序目录 +│ ├── main.c # 主程序文件 (3025行) +│ ├── CMakeLists.txt # 构建配置 +│ └── idf_component.yml # 组件依赖 +├── components/ # 自定义组件 +│ ├── lvgl_st7735s_use/ # LVGL显示组件 +│ ├── serial_mcu/ # 串口通信组件 +│ ├── ahtxx/ # 温湿度传感器组件 +│ ├── bh1750/ # 光照传感器组件 +│ └── iot_servo/ # 舵机控制组件 +├── managed_components/ # 管理组件 (自动下载) +│ └── espp__wifi/ # Wi-Fi连接组件 +├── build/ # 编译输出目录 +├── sdkconfig # ESP-IDF配置文件 +├── partitions.csv # 分区表 +└── README.md # 本文档 +``` + +--- + +## 许可证 + +MIT License + +--- + +## 作者 + +**beihong.wang** + +--- + +## 更新日志 + +### v1.0 (2026-01-19) + +- 初始版本发布 +- 完整的智能家居控制功能 +- MQTT远程控制 +- LVGL本地显示 +- 多闹钟系统 +- 自动通风和降温模式 + +--- + +## 贡献 + +欢迎提交Issue和Pull Request! + +--- + +## 联系方式 + +如有问题或建议,请通过以下方式联系: + +- 提交Issue +- 发送邮件 + +--- + +**文档生成时间**: 2026年2月7日 +**文档版本**: 1.0 diff --git a/components/lvgl_st7735s_use/CMakeLists.txt b/components/lvgl_st7735s_use/CMakeLists.txt new file mode 100644 index 0000000..f6799d1 --- /dev/null +++ b/components/lvgl_st7735s_use/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "ui_display.c" "lvgl_st7735s_use.c" + INCLUDE_DIRS "include" + REQUIRES driver esp_lcd esp_lvgl_port + ) \ No newline at end of file diff --git a/components/lvgl_st7735s_use/include/lvgl_st7735s_use.h b/components/lvgl_st7735s_use/include/lvgl_st7735s_use.h new file mode 100644 index 0000000..b5482ce --- /dev/null +++ b/components/lvgl_st7735s_use/include/lvgl_st7735s_use.h @@ -0,0 +1,33 @@ + +/* LCD size */ +#define EXAMPLE_LCD_H_RES (160) +#define EXAMPLE_LCD_V_RES (80) + +/* LCD SPI总线配置 */ +#define EXAMPLE_LCD_SPI_NUM (SPI2_HOST) // 使用SPI2主机接口进行通信 + +/* LCD显示参数配置 */ +#define EXAMPLE_LCD_PIXEL_CLK_HZ (40 * 1000 * 1000) // 像素时钟频率设置为40MHz,控制数据传输速度 + +/* LCD命令和参数配置 */ +#define EXAMPLE_LCD_CMD_BITS (8) // 命令位数为8位,用于发送LCD控制命令 +#define EXAMPLE_LCD_PARAM_BITS (8) // 参数位数为8位,用于发送命令参数 + +/* LCD颜色和缓冲区配置 */ +#define EXAMPLE_LCD_BITS_PER_PIXEL (16) // 每个像素使用16位颜色(RGB565格式) +#define EXAMPLE_LCD_DRAW_BUFF_DOUBLE (1) // 启用双缓冲模式,提高显示流畅度 +#define EXAMPLE_LCD_DRAW_BUFF_HEIGHT (50) // 绘图缓冲区高度为50行,影响刷新性能 + +/* LCD背光配置 */ +#define EXAMPLE_LCD_BL_ON_LEVEL (1) // 背光开启电平为高电平(1) + +/* LCD pins */ +#define EXAMPLE_LCD_GPIO_SCLK (GPIO_NUM_2) +#define EXAMPLE_LCD_GPIO_MOSI (GPIO_NUM_3) +#define EXAMPLE_LCD_GPIO_RST (GPIO_NUM_9) +#define EXAMPLE_LCD_GPIO_DC (GPIO_NUM_8) +#define EXAMPLE_LCD_GPIO_CS (GPIO_NUM_7) +#define EXAMPLE_LCD_GPIO_BL (GPIO_NUM_6) + + +void start_lvgl_demo(void); diff --git a/components/lvgl_st7735s_use/include/ui_display.h b/components/lvgl_st7735s_use/include/ui_display.h new file mode 100644 index 0000000..422b6b3 --- /dev/null +++ b/components/lvgl_st7735s_use/include/ui_display.h @@ -0,0 +1,38 @@ +#ifndef UI_DISPLAY_H +#define UI_DISPLAY_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief 初始化UI界面 + * + * 该函数负责创建LVGL的用户界面元素,用于显示传感器数据 + */ +void ui_display_init(void); + +/** + * @brief 更新传感器数据显示 + * + * 该函数用于更新LVGL界面上的传感器数据 + * + * @param temperature 温度值(°C),-1.0表示无效 + * @param humidity 湿度值(%),-1.0表示无效 + * @param lux 光照强度(lx),-1.0表示无效 + * @param ppm 空气中有害气体浓度(ppm) + * @param quality_level 空气质量等级描述 + */ +void ui_update_sensor_data(float temperature, float humidity, float lux, float ppm, const char* quality_level); + +/* Time page APIs */ +void ui_show_time_page(void); +void ui_show_sensor_page(void); +void ui_time_update(void); +void ui_toggle_page(void); + +#ifdef __cplusplus +} +#endif + +#endif /* UI_DISPLAY_H */ \ No newline at end of file diff --git a/components/lvgl_st7735s_use/lvgl_st7735s_use.c b/components/lvgl_st7735s_use/lvgl_st7735s_use.c new file mode 100644 index 0000000..25b7267 --- /dev/null +++ b/components/lvgl_st7735s_use/lvgl_st7735s_use.c @@ -0,0 +1,236 @@ +#include +#include "lvgl_st7735s_use.h" +#include "driver/gpio.h" +#include "driver/spi_master.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_check.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lvgl_port.h" // 包含LVGL端口头文件,用于LVGL与ESP硬件的接口 + +#include "ui_display.h" // 添加新UI界面的头文件 +static const char *TAG = "lvgl_st7735s_use"; // 用于日志输出的标签,便于调试时识别日志来源 + + +/* LCD IO和面板句柄 */ +// lcd_io: LCD面板IO句柄,用于与LCD进行通信 +// lcd_panel: LCD面板句柄,用于控制LCD的各种操作 +static esp_lcd_panel_io_handle_t lcd_io = NULL; +static esp_lcd_panel_handle_t lcd_panel = NULL; + +/* LVGL显示和触摸 */ +// lvgl_disp: LVGL显示设备句柄,用于LVGL库与显示设备的交互 +static lv_display_t *lvgl_disp = NULL; + + + +/** + * @brief 初始化LCD硬件和SPI接口 + * + * 该函数负责初始化LCD所需的GPIO、SPI总线,并配置LCD面板 + * 包括背光控制、SPI总线配置、面板IO配置和面板驱动安装 + * + * @return esp_err_t 初始化结果,ESP_OK表示成功 + */ +static esp_err_t app_lcd_init(void) +{ + esp_err_t ret = ESP_OK; + + /* LCD背光配置 */ + // 配置背光GPIO为输出模式,用于控制LCD的背光开关 + gpio_config_t bk_gpio_config = { + .mode = GPIO_MODE_OUTPUT, // 设置GPIO为输出模式 + .pin_bit_mask = 1ULL << EXAMPLE_LCD_GPIO_BL // 设置背光GPIO引脚 + }; + ESP_ERROR_CHECK(gpio_config(&bk_gpio_config)); // 应用GPIO配置并检查错误 + + /* LCD初始化 */ + ESP_LOGD(TAG, "初始化SPI总线"); // 输出调试日志 + // 配置SPI总线参数,包括时钟、数据线和最大传输大小 + const spi_bus_config_t buscfg = { + .sclk_io_num = EXAMPLE_LCD_GPIO_SCLK, // SPI时钟引脚 + .mosi_io_num = EXAMPLE_LCD_GPIO_MOSI, // SPI主输出从输入引脚 + .miso_io_num = GPIO_NUM_NC, // 未使用MISO引脚 + .quadwp_io_num = GPIO_NUM_NC, // 未使用WP引脚 + .quadhd_io_num = GPIO_NUM_NC, // 未使用HD引脚 + .max_transfer_sz = EXAMPLE_LCD_H_RES * EXAMPLE_LCD_DRAW_BUFF_HEIGHT * sizeof(uint16_t), // 最大传输大小 + }; + // 初始化SPI总线,使用DMA自动分配通道 + ESP_RETURN_ON_ERROR(spi_bus_initialize(EXAMPLE_LCD_SPI_NUM, &buscfg, SPI_DMA_CH_AUTO), TAG, "SPI初始化失败"); + + ESP_LOGD(TAG, "安装面板IO"); + // 配置LCD面板IO的SPI参数 + const esp_lcd_panel_io_spi_config_t io_config = { + .dc_gpio_num = EXAMPLE_LCD_GPIO_DC, // 数据/命令选择引脚 + .cs_gpio_num = EXAMPLE_LCD_GPIO_CS, // 片选引脚 + .pclk_hz = EXAMPLE_LCD_PIXEL_CLK_HZ, // 像素时钟频率 + .lcd_cmd_bits = EXAMPLE_LCD_CMD_BITS, // 命令位数 + .lcd_param_bits = EXAMPLE_LCD_PARAM_BITS, // 参数位数 + .spi_mode = 3, // SPI模式 + .trans_queue_depth = 10, // 传输队列深度 + }; + // 创建LCD面板IO,用于SPI通信 + ESP_GOTO_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)EXAMPLE_LCD_SPI_NUM, &io_config, &lcd_io), err, TAG, "创建面板IO失败"); + + ESP_LOGD(TAG, "安装LCD驱动"); + // 配置LCD面板设备参数 + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = EXAMPLE_LCD_GPIO_RST, // 复位引脚 +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) + .rgb_endian = LCD_RGB_ENDIAN_RGB, // RGB字节序(旧版本) +#else + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, // RGB元素顺序(新版本) +#endif + .bits_per_pixel = EXAMPLE_LCD_BITS_PER_PIXEL, // 每像素位数 + }; + // 创建ST7789 LCD面板驱动 + ESP_GOTO_ON_ERROR(esp_lcd_new_panel_st7789(lcd_io, &panel_config, &lcd_panel), err, TAG, "创建面板失败"); + + // 复位LCD面板 + esp_lcd_panel_reset(lcd_panel); + // 初始化LCD面板 + esp_lcd_panel_init(lcd_panel); + // 设置显示窗口,确保使用正确的分辨率(偏移) + esp_lcd_panel_set_gap(lcd_panel, 1, 26); + + // 反转颜色 + esp_lcd_panel_invert_color(lcd_panel, true); + // 打开LCD显示 + esp_lcd_panel_disp_on_off(lcd_panel, true); + + /* 打开LCD背光 */ + ESP_ERROR_CHECK(gpio_set_level(EXAMPLE_LCD_GPIO_BL, EXAMPLE_LCD_BL_ON_LEVEL)); + + return ret; + +// 错误处理标签,用于清理资源 +err: + if (lcd_panel) { + esp_lcd_panel_del(lcd_panel); // 删除面板 + } + if (lcd_io) { + esp_lcd_panel_io_del(lcd_io); // 删除面板IO + } + spi_bus_free(EXAMPLE_LCD_SPI_NUM); // 释放SPI总线资源 + return ret; +} + + +/** + * @brief 初始化LVGL图形库 + * + * 该函数负责初始化LVGL库,并配置显示设备 + * 包括LVGL任务配置、显示缓冲区配置和旋转设置 + * + * @return esp_err_t 初始化结果,ESP_OK表示成功 + */ +static esp_err_t app_lvgl_init(void) +{ + /* 初始化LVGL */ + // 配置LVGL任务参数 + const lvgl_port_cfg_t lvgl_cfg = { + .task_priority = 4, /* LVGL任务优先级,数值越高优先级越高 */ + .task_stack = 4096, /* LVGL任务堆栈大小,单位为字节 */ + .task_affinity = -1, /* LVGL任务绑定核心,-1表示不绑定特定核心 */ + .task_max_sleep_ms = 500, /* LVGL任务最大睡眠时间,单位为毫秒 */ + .timer_period_ms = 5 /* LVGL定时器周期,单位为毫秒,用于处理动画和输入 */ + }; + // 初始化LVGL端口 + ESP_RETURN_ON_ERROR(lvgl_port_init(&lvgl_cfg), TAG, "LVGL端口初始化失败"); + + /* 添加LCD屏幕 */ + ESP_LOGD(TAG, "添加LCD屏幕"); + // 配置LVGL显示设备参数 + const lvgl_port_display_cfg_t disp_cfg = { + .io_handle = lcd_io, // LCD面板IO句柄 + .panel_handle = lcd_panel, // LCD面板句柄 + .buffer_size = EXAMPLE_LCD_H_RES * EXAMPLE_LCD_DRAW_BUFF_HEIGHT, // 缓冲区大小 + .double_buffer = EXAMPLE_LCD_DRAW_BUFF_DOUBLE, // 是否使用双缓冲 + .hres = EXAMPLE_LCD_H_RES, // 水平分辨率 + .vres = EXAMPLE_LCD_V_RES, // 垂直分辨率 + .monochrome = false, // 是否为单色显示 +#if LVGL_VERSION_MAJOR >= 9 + .color_format = LV_COLOR_FORMAT_RGB565, // 颜色格式(LVGL v9及以上) +#endif + .rotation = { // 旋转设置 + .swap_xy = 1, // 交换X和Y轴以实现横向显示 + .mirror_x = 1, // 是否水平镜像 + .mirror_y = 0, // 是否垂直镜像 + }, + .flags = { // 标志位 + .buff_dma = true, // 是否使用DMA缓冲区 +#if LVGL_VERSION_MAJOR >= 9 + .swap_bytes = false, // 是否交换字节序(LVGL v9及以上) +#endif + } + }; + // 添加LVGL显示设备 + lvgl_disp = lvgl_port_add_disp(&disp_cfg); + + return ESP_OK; +} + + + +/** + * @brief 创建并显示LVGL主界面 + * + * 该函数负责创建LVGL的用户界面元素,包括图像、标签和按钮 + * 并设置它们的位置和属性 + */ +static void app_main_display(void) +{ + // 获取当前活动屏幕对象 + lv_obj_t *scr = lv_scr_act(); + + /* 任务锁定 */ + // 锁定LVGL任务,防止在创建UI对象时被中断 + lvgl_port_lock(0); + + /* 设置屏幕背景为黑色 */ + lv_obj_set_style_bg_color(scr, lv_color_white(), 0); + lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); + + + /* 创建标签 */ + // 创建标签对象 + lv_obj_t *label = lv_label_create(scr); + // 设置标签文本为"ESP32C3-LVGL" + lv_label_set_text(label, "ESP32C3-LVGL1"); + // 设置标签文本颜色为白色 + lv_obj_set_style_text_color(label, lv_color_black(), 0); + // 设置标签文本字体大小为更小的字体 + lv_obj_set_style_text_font(label, &lv_font_unscii_8, 0); + + // 设置标签位置在屏幕中心 + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + + /* 任务解锁 */ + // 解锁LVGL任务 + lvgl_port_unlock(); +} + +/** + * @brief 启动LVGL演示程序 + * + * 该函数是程序的入口点,负责初始化LCD硬件、LVGL库,并显示主界面 + */ +void start_lvgl_demo(void) +{ + /* LCD硬件初始化 */ + // 初始化LCD硬件和SPI接口 + ESP_ERROR_CHECK(app_lcd_init()); + + /* LVGL初始化 */ + // 初始化LVGL图形库 + ESP_ERROR_CHECK(app_lvgl_init()); + + /* 显示LVGL对象 */ + // 创建并显示LVGL主界面 + // app_main_display(); + + /* 显示LVGL对象 - 使用新的UI界面初始化函数 */ + ui_display_init(); +} diff --git a/components/lvgl_st7735s_use/ui_display.c b/components/lvgl_st7735s_use/ui_display.c new file mode 100644 index 0000000..8c794f4 --- /dev/null +++ b/components/lvgl_st7735s_use/ui_display.c @@ -0,0 +1,258 @@ +#include "ui_display.h" +#include "esp_log.h" +#include "lvgl.h" +#include "esp_lvgl_port.h" +#include + +static const char *TAG = "ui_display"; + +// 全局变量用于存储传感器数据标签 +static lv_obj_t *temp_label = NULL; +static lv_obj_t *humid_label = NULL; +static lv_obj_t *lux_label = NULL; +static lv_obj_t *air_quality_label = NULL; // 新增空气质量标签 + +/* Time page objects */ +static lv_obj_t *time_container = NULL; +static lv_obj_t *date_label = NULL; +static lv_obj_t *time_label = NULL; +static bool time_page_visible = false; + +/** + * @brief 初始化UI界面 + * + * 该函数负责创建LVGL的用户界面元素,用于显示传感器数据 + * 优化布局以适应非触摸屏设备,所有内容在一个屏幕内显示 + */ +void ui_display_init(void) +{ + // 获取当前活动屏幕对象 + lv_obj_t *scr = lv_scr_act(); + + /* 任务锁定 */ + lvgl_port_lock(0); + + /* 设置屏幕背景为白色 */ + lv_obj_set_style_bg_color(scr, lv_color_white(), 0); + lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); + + /* 创建标题标签 */ + lv_obj_t *title_label = lv_label_create(scr); + lv_label_set_text(title_label, "IoT Home Monitor"); + lv_obj_set_style_text_color(title_label, lv_palette_main(LV_PALETTE_BLUE), 0); + lv_obj_set_style_text_font(title_label, &lv_font_unscii_8, 0); + lv_obj_align(title_label, LV_ALIGN_TOP_MID, 0, 2); // 调整标题位置 + + /* 创建温度标签 */ + temp_label = lv_label_create(scr); + lv_label_set_text(temp_label, "Temp: --.- C"); + lv_obj_set_style_text_color(temp_label, lv_color_black(), 0); + lv_obj_set_style_text_font(temp_label, &lv_font_unscii_8, 0); + lv_obj_align(temp_label, LV_ALIGN_TOP_LEFT, 3, 20); // 调整位置 + + /* 创建湿度标签 */ + humid_label = lv_label_create(scr); + lv_label_set_text(humid_label, "Humidity: --.- %"); + lv_obj_set_style_text_color(humid_label, lv_color_black(), 0); + lv_obj_set_style_text_font(humid_label, &lv_font_unscii_8, 0); + lv_obj_align(humid_label, LV_ALIGN_TOP_LEFT, 3, 35); // 调整位置 + + /* 创建光照标签 */ + lux_label = lv_label_create(scr); + lv_label_set_text(lux_label, "Light: --.- lux"); + lv_obj_set_style_text_color(lux_label, lv_color_black(), 0); + lv_obj_set_style_text_font(lux_label, &lv_font_unscii_8, 0); + lv_obj_align(lux_label, LV_ALIGN_TOP_LEFT, 3, 50); // 调整位置 + + /* 创建空气质量标签 */ + air_quality_label = lv_label_create(scr); + lv_label_set_text(air_quality_label, "IAQ : --.- Index"); + lv_obj_set_style_text_color(air_quality_label, lv_color_black(), 0); + lv_obj_set_style_text_font(air_quality_label, &lv_font_unscii_8, 0); + lv_obj_align(air_quality_label, LV_ALIGN_TOP_LEFT, 3, 65); // 调整位置 + + /* 任务解锁 */ + lvgl_port_unlock(); + + // 创建时间页面(初始隐藏) + lvgl_port_lock(0); + time_container = lv_obj_create(lv_scr_act()); + lv_obj_set_size(time_container, lv_pct(100), lv_pct(100)); + lv_obj_set_style_bg_color(time_container, lv_color_white(), 0); + lv_obj_set_style_bg_opa(time_container, LV_OPA_COVER, 0); + + date_label = lv_label_create(time_container); + lv_label_set_text(date_label, "---- ---- -- ---"); + lv_obj_set_style_text_color(date_label, lv_color_black(), 0); + lv_obj_set_style_text_font(date_label, &lv_font_unscii_8, 0); + lv_obj_align(date_label, LV_ALIGN_TOP_MID, 0, 6); + + time_label = lv_label_create(time_container); + lv_label_set_text(time_label, "--:--:--"); + lv_obj_set_style_text_color(time_label, lv_color_black(), 0); + lv_obj_set_style_text_font(time_label, &lv_font_unscii_16, 0); + lv_obj_align(time_label, LV_ALIGN_CENTER, 0, 12); + + // 默认显示时间页面,隐藏传感器页面 + lv_obj_clear_flag(time_container, LV_OBJ_FLAG_HIDDEN); + if (temp_label) lv_obj_add_flag(temp_label, LV_OBJ_FLAG_HIDDEN); + if (humid_label) lv_obj_add_flag(humid_label, LV_OBJ_FLAG_HIDDEN); + if (lux_label) lv_obj_add_flag(lux_label, LV_OBJ_FLAG_HIDDEN); + if (air_quality_label) lv_obj_add_flag(air_quality_label, LV_OBJ_FLAG_HIDDEN); + time_page_visible = true; + lvgl_port_unlock(); +} + +/** + * @brief 更新传感器数据显示 + * + * 该函数用于更新LVGL界面上的传感器数据 + * + * @param temperature 温度值(°C),-1.0表示无效 + * @param humidity 湿度值(%),-1.0表示无效 + * @param lux 光照强度(lx),-1.0表示无效 + * @param ppm 空气中有害气体浓度(ppm) + * @param quality_level 空气质量等级描述 + */ +void ui_update_sensor_data(float temperature, float humidity, float lux, float ppm, const char* quality_level) +{ + if (temp_label != NULL && humid_label != NULL && lux_label != NULL && air_quality_label != NULL) + { + /* 任务锁定 */ + lvgl_port_lock(0); + + // 更新温度标签 - 缩短文本以节省空间 + if (temperature >= -0.5) // -1.0表示无效 + { + char temp_str[32]; + snprintf(temp_str, sizeof(temp_str), "Temp: %.2f C", temperature); + lv_label_set_text(temp_label, temp_str); + } + else + { + lv_label_set_text(temp_label, "Temp: Invalid"); + } + + // 更新湿度标签 - 缩短文本以节省空间 + if (humidity >= -0.5) // -1.0表示无效 + { + char humid_str[32]; + snprintf(humid_str, sizeof(humid_str), "Humidity: %.2f %%", humidity); + lv_label_set_text(humid_label, humid_str); + } + else + { + lv_label_set_text(humid_label, "Humidity: Invalid"); + } + + // 更新光照标签 - 缩短文本以节省空间 + if (lux >= -0.5) // -1.0表示无效 + { + char lux_str[32]; + snprintf(lux_str, sizeof(lux_str), "Light: %.2f lx", lux); + lv_label_set_text(lux_label, lux_str); + } + else + { + lv_label_set_text(lux_label, "Light: Invalid"); + } + + // 更新空气质量标签 - 缩短文本以节省空间 + if (ppm >= 0) // 空气质量值有效 + { + char ppm_str[32]; + snprintf(ppm_str, sizeof(ppm_str), "IAQ : %.2f Index", ppm); + + // 根据空气质量等级更改颜色 + lv_color_t color = lv_color_black(); // 默认黑色 + if (ppm <= 20.0f) { + color = lv_color_make(0, 128, 0); // 绿色 - 空气质量优秀 + } else if (ppm <= 100.0f) { + color = lv_color_make(0, 0, 0); // 黑色 - 空气质量良好 + } else if (ppm <= 300.0f) { + color = lv_color_make(255, 165, 0); // 橙色 - 轻度污染 + } else { + color = lv_color_make(255, 0, 0); // 红色 - 中重度污染 + } + + lv_label_set_text(air_quality_label, ppm_str); + lv_obj_set_style_text_color(air_quality_label, color, 0); + } + else + { + lv_label_set_text(air_quality_label, "IAQ : Invalid"); + lv_obj_set_style_text_color(air_quality_label, lv_color_black(), 0); + } + + /* 任务解锁 */ + lvgl_port_unlock(); + } +} + +/* Show time page */ +void ui_show_time_page(void) +{ + lvgl_port_lock(0); + if (time_container) + { + lv_obj_clear_flag(time_container, LV_OBJ_FLAG_HIDDEN); + } + if (temp_label) lv_obj_add_flag(temp_label, LV_OBJ_FLAG_HIDDEN); + if (humid_label) lv_obj_add_flag(humid_label, LV_OBJ_FLAG_HIDDEN); + if (lux_label) lv_obj_add_flag(lux_label, LV_OBJ_FLAG_HIDDEN); + if (air_quality_label) lv_obj_add_flag(air_quality_label, LV_OBJ_FLAG_HIDDEN); + time_page_visible = true; + lvgl_port_unlock(); +} + +/* Show sensor page */ +void ui_show_sensor_page(void) +{ + lvgl_port_lock(0); + if (time_container) + { + lv_obj_add_flag(time_container, LV_OBJ_FLAG_HIDDEN); + } + if (temp_label) lv_obj_clear_flag(temp_label, LV_OBJ_FLAG_HIDDEN); + if (humid_label) lv_obj_clear_flag(humid_label, LV_OBJ_FLAG_HIDDEN); + if (lux_label) lv_obj_clear_flag(lux_label, LV_OBJ_FLAG_HIDDEN); + if (air_quality_label) lv_obj_clear_flag(air_quality_label, LV_OBJ_FLAG_HIDDEN); + time_page_visible = false; + lvgl_port_unlock(); +} + +/* Toggle between pages */ +void ui_toggle_page(void) +{ + if (time_page_visible) + ui_show_sensor_page(); + else + ui_show_time_page(); +} + +/* Update time label (call periodically) */ +void ui_time_update(void) +{ + if (!time_page_visible || time_label == NULL) + return; + + time_t now = time(NULL); + struct tm tm_now; + localtime_r(&now, &tm_now); + + char date_buf[64]; + char time_buf[32]; + char weekday_buf[32]; + + // 年月日和星期合并为一行 + strftime(date_buf, sizeof(date_buf), "%Y-%m-%d", &tm_now); + const char *weekdays[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + snprintf(date_buf + strlen(date_buf), sizeof(date_buf) - strlen(date_buf), " %s", weekdays[tm_now.tm_wday]); + // 时分秒 + strftime(time_buf, sizeof(time_buf), "%H:%M:%S", &tm_now); + + lvgl_port_lock(0); + lv_label_set_text(date_label, date_buf); + lv_label_set_text(time_label, time_buf); + lvgl_port_unlock(); +} diff --git a/components/protocol_examples_common/CMakeLists.txt b/components/protocol_examples_common/CMakeLists.txt new file mode 100644 index 0000000..8d0501a --- /dev/null +++ b/components/protocol_examples_common/CMakeLists.txt @@ -0,0 +1,52 @@ +idf_build_get_property(target IDF_TARGET) + +if(${target} STREQUAL "linux") + # Header only library for linux + + idf_component_register(INCLUDE_DIRS include + SRCS protocol_examples_utils.c) + return() +endif() + +set(srcs "stdin_out.c" + "addr_from_stdin.c" + "connect.c" + "wifi_connect.c" + "protocol_examples_utils.c") + +if(CONFIG_EXAMPLE_PROVIDE_WIFI_CONSOLE_CMD) + list(APPEND srcs "console_cmd.c") +endif() + +if(CONFIG_EXAMPLE_CONNECT_ETHERNET) + list(APPEND srcs "eth_connect.c") +endif() + +if(CONFIG_EXAMPLE_CONNECT_THREAD) + list(APPEND srcs "thread_connect.c") +endif() + +if(CONFIG_EXAMPLE_CONNECT_PPP) + list(APPEND srcs "ppp_connect.c") +endif() + + +idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS "include" + PRIV_REQUIRES esp_netif esp_driver_gpio esp_driver_uart esp_wifi vfs console esp_eth openthread) + +if(CONFIG_EXAMPLE_PROVIDE_WIFI_CONSOLE_CMD) + idf_component_optional_requires(PRIVATE console) +endif() + +if(CONFIG_EXAMPLE_CONNECT_ETHERNET) + idf_component_optional_requires(PUBLIC esp_eth) +endif() + +if(CONFIG_EXAMPLE_CONNECT_THREAD) + idf_component_optional_requires(PRIVATE openthread) +endif() + +if(CONFIG_EXAMPLE_CONNECT_PPP) + idf_component_optional_requires(PRIVATE esp_tinyusb espressif__esp_tinyusb) +endif() diff --git a/components/protocol_examples_common/CMakeLists.txt:Zone.Identifier b/components/protocol_examples_common/CMakeLists.txt:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2xComponents->OpenThread->Thread Core Features->Thread Operational Dataset' + + if EXAMPLE_CONNECT_THREAD + config EXAMPLE_THREAD_TASK_STACK_SIZE + int "Example Thread task stack size" + default 8192 + help + Thread task stack size + + menu "Radio Spinel Options" + depends on OPENTHREAD_RADIO_SPINEL_UART || OPENTHREAD_RADIO_SPINEL_SPI + + config EXAMPLE_THREAD_UART_RX_PIN + depends on OPENTHREAD_RADIO_SPINEL_UART + int "Uart Rx Pin" + default 17 + + config EXAMPLE_THREAD_UART_TX_PIN + depends on OPENTHREAD_RADIO_SPINEL_UART + int "Uart Tx pin" + default 18 + + config EXAMPLE_THREAD_UART_BAUD + depends on OPENTHREAD_RADIO_SPINEL_UART + int "Uart baud rate" + default 460800 + + config EXAMPLE_THREAD_UART_PORT + depends on OPENTHREAD_RADIO_SPINEL_UART + int "Uart port" + default 1 + + config EXAMPLE_THREAD_SPI_CS_PIN + depends on OPENTHREAD_RADIO_SPINEL_SPI + int "SPI CS Pin" + default 10 + + config EXAMPLE_THREAD_SPI_SCLK_PIN + depends on OPENTHREAD_RADIO_SPINEL_SPI + int "SPI SCLK Pin" + default 12 + + config EXAMPLE_THREAD_SPI_MISO_PIN + depends on OPENTHREAD_RADIO_SPINEL_SPI + int "SPI MISO Pin" + default 13 + + config EXAMPLE_THREAD_SPI_MOSI_PIN + depends on OPENTHREAD_RADIO_SPINEL_SPI + int "SPI MOSI Pin" + default 11 + + config EXAMPLE_THREAD_SPI_INTR_PIN + depends on OPENTHREAD_RADIO_SPINEL_SPI + int "SPI Interrupt Pin" + default 8 + endmenu + + endif + + config EXAMPLE_CONNECT_IPV4 + bool + depends on LWIP_IPV4 + default n if EXAMPLE_CONNECT_THREAD + default y + + config EXAMPLE_CONNECT_IPV6 + depends on EXAMPLE_CONNECT_WIFI || EXAMPLE_CONNECT_ETHERNET || EXAMPLE_CONNECT_PPP || EXAMPLE_CONNECT_THREAD + bool "Obtain IPv6 address" + default y + select LWIP_IPV6 + select LWIP_PPP_ENABLE_IPV6 if EXAMPLE_CONNECT_PPP + help + By default, examples will wait until IPv4 and IPv6 local link addresses are obtained. + Disable this option if the network does not support IPv6. + Choose the preferred IPv6 address type if the connection code should wait until other than + the local link address gets assigned. + Consider enabling IPv6 stateless address autoconfiguration (SLAAC) in the LWIP component. + + if EXAMPLE_CONNECT_IPV6 + choice EXAMPLE_CONNECT_PREFERRED_IPV6 + prompt "Preferred IPv6 Type" + default EXAMPLE_CONNECT_IPV6_PREF_LOCAL_LINK + help + Select which kind of IPv6 address the connect logic waits for. + + config EXAMPLE_CONNECT_IPV6_PREF_LOCAL_LINK + bool "Local Link Address" + help + Blocks until Local link address assigned. + + config EXAMPLE_CONNECT_IPV6_PREF_GLOBAL + bool "Global Address" + help + Blocks until Global address assigned. + + config EXAMPLE_CONNECT_IPV6_PREF_SITE_LOCAL + bool "Site Local Address" + help + Blocks until Site link address assigned. + + config EXAMPLE_CONNECT_IPV6_PREF_UNIQUE_LOCAL + bool "Unique Local Link Address" + help + Blocks until Unique local address assigned. + + endchoice + + endif + + +endmenu diff --git a/components/protocol_examples_common/Kconfig.projbuild:Zone.Identifier b/components/protocol_examples_common/Kconfig.projbuild:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2xComponents->OpenThread->Thread Core Features->Thread Operational Dataset'. + +If the Thread end-device joins a Thread network with a Thread Border Router that has the NAT64 feature enabled, the end-device can access the Internet with the standard DNS APIs after configuring the following properties: +* Enable DNS64 client ('->Components->OpenThread->Thread Core Features->Enable DNS64 client') +* Enable custom DNS external resolve Hook ('->Components->LWIP->Hooks->DNS external resolve Hook->Custom implementation') + +### PPP + +Point to point connection method creates a simple IP tunnel to the counterpart device (running PPP server), typically a Linux machine with pppd service. We currently support only PPP over Serial (using UART or USB CDC). This is useful for simple testing of networking layers, but with some additional configuration on the server side, we could simulate standard model of internet connectivity. The PPP server could be also represented by a cellular modem device with pre-configured connectivity and already switched to PPP mode (this setup is not very flexible though, so we suggest using a standard modem library implementing commands and modes, e.g. [esp_modem](https://components.espressif.com/component/espressif/esp_modem) ). + +> [!Note] +> Note that if you choose USB device, you have to manually add a dependency on `esp_tinyusb` component. This step is necessary to keep the `protocol_example_connect` component simple and dependency free. Please run this command from your project location to add the dependency: +> ```bash +> idf.py add-dependency espressif/esp_tinyusb^1 +> ``` + +#### Setup a PPP server + +Connect the board using UART or USB and note the device name, which would be typically: +* `/dev/ttyACMx` for USB devices +* `/dev/ttyUSBx` for UART devices + +Run the pppd server: + +```bash +sudo pppd /dev/ttyACM0 115200 192.168.11.1:192.168.11.2 ms-dns 8.8.8.8 modem local noauth debug nocrtscts nodetach +ipv6 +``` + +Please update the parameters with the correct serial device, baud rate, IP addresses, DNS server, use `+ipv6` if `EXAMPLE_CONNECT_IPV6=y`. + +#### Connection to outside + +In order to access other network endpoints, we have to configure some IP/translation rules. The easiest method is to setup a masquerade of the PPPD created interface (`ppp0`) to your default networking interface (`${ETH0}`). Here is an example of such rule: + +```bash +sudo iptables -t nat -A POSTROUTING -o ${ETH0} -j MASQUERADE +sudo iptables -A FORWARD -i ${ETH0} -o ppp0 -m state --state RELATED,ESTABLISHED -j ACCEPT +sudo iptables -A FORWARD -i ppp0 -o ${ETH0} -j ACCEPT +``` diff --git a/components/protocol_examples_common/README.md:Zone.Identifier b/components/protocol_examples_common/README.md:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x +#include "esp_system.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "protocol_examples_common.h" + +#include "lwip/sockets.h" +#include +#include + +#define HOST_IP_SIZE 128 + +esp_err_t get_addr_from_stdin(int port, int sock_type, int *ip_protocol, int *addr_family, struct sockaddr_storage *dest_addr) +{ + char host_ip[HOST_IP_SIZE]; + int len; + static bool already_init = false; + + // this function could be called multiple times -> make sure UART init runs only once + if (!already_init) { + example_configure_stdin_stdout(); + already_init = true; + } + + // ignore empty or LF only string (could receive from DUT class) + do { + fgets(host_ip, HOST_IP_SIZE, stdin); + len = strlen(host_ip); + } while (len<=1 && host_ip[0] == '\n'); + host_ip[len - 1] = '\0'; + + struct addrinfo hints, *addr_list, *cur; + memset( &hints, 0, sizeof( hints ) ); + + // run getaddrinfo() to decide on the IP protocol + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = sock_type; + hints.ai_protocol = IPPROTO_TCP; + if( getaddrinfo( host_ip, NULL, &hints, &addr_list ) != 0 ) { + return ESP_FAIL; + } + for( cur = addr_list; cur != NULL; cur = cur->ai_next ) { + memcpy(dest_addr, cur->ai_addr, sizeof(*dest_addr)); +#if CONFIG_EXAMPLE_CONNECT_IPV4 + if (cur->ai_family == AF_INET) { + *ip_protocol = IPPROTO_IP; + *addr_family = AF_INET; + // add port number and return on first IPv4 match + ((struct sockaddr_in*)dest_addr)->sin_port = htons(port); + freeaddrinfo( addr_list ); + return ESP_OK; + } +#endif // IPV4 +#if CONFIG_EXAMPLE_CONNECT_IPV6 + if (cur->ai_family == AF_INET6) { + *ip_protocol = IPPROTO_IPV6; + *addr_family = AF_INET6; + // add port and interface number and return on first IPv6 match + ((struct sockaddr_in6*)dest_addr)->sin6_port = htons(port); + ((struct sockaddr_in6*)dest_addr)->sin6_scope_id = esp_netif_get_netif_impl_index(EXAMPLE_INTERFACE); + freeaddrinfo( addr_list ); + return ESP_OK; + } +#endif // IPV6 + } + // no match found + freeaddrinfo( addr_list ); + return ESP_FAIL; +} diff --git a/components/protocol_examples_common/addr_from_stdin.c:Zone.Identifier b/components/protocol_examples_common/addr_from_stdin.c:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x +#include "protocol_examples_common.h" +#include "example_common_private.h" +#include "sdkconfig.h" +#include "esp_event.h" +#include "esp_wifi.h" +#include "esp_wifi_default.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "lwip/err.h" +#include "lwip/sys.h" + +static const char *TAG = "example_common"; + +#if CONFIG_EXAMPLE_CONNECT_IPV6 +/* types of ipv6 addresses to be displayed on ipv6 events */ +const char *example_ipv6_addr_types_to_str[6] = { + "ESP_IP6_ADDR_IS_UNKNOWN", + "ESP_IP6_ADDR_IS_GLOBAL", + "ESP_IP6_ADDR_IS_LINK_LOCAL", + "ESP_IP6_ADDR_IS_SITE_LOCAL", + "ESP_IP6_ADDR_IS_UNIQUE_LOCAL", + "ESP_IP6_ADDR_IS_IPV4_MAPPED_IPV6" +}; +#endif + +/** + * @brief Checks the netif description if it contains specified prefix. + * All netifs created within common connect component are prefixed with the module TAG, + * so it returns true if the specified netif is owned by this module + */ +bool example_is_our_netif(const char *prefix, esp_netif_t *netif) +{ + return strncmp(prefix, esp_netif_get_desc(netif), strlen(prefix) - 1) == 0; +} + +static bool netif_desc_matches_with(esp_netif_t *netif, void *ctx) +{ + return strcmp(ctx, esp_netif_get_desc(netif)) == 0; +} + +esp_netif_t *get_example_netif_from_desc(const char *desc) +{ + return esp_netif_find_if(netif_desc_matches_with, (void*)desc); +} + +static esp_err_t print_all_ips_tcpip(void* ctx) +{ + const char *prefix = ctx; + // iterate over active interfaces, and print out IPs of "our" netifs + esp_netif_t *netif = NULL; + while ((netif = esp_netif_next_unsafe(netif)) != NULL) { + if (example_is_our_netif(prefix, netif)) { + ESP_LOGI(TAG, "Connected to %s", esp_netif_get_desc(netif)); +#if CONFIG_EXAMPLE_CONNECT_IPV4 + esp_netif_ip_info_t ip; + ESP_ERROR_CHECK(esp_netif_get_ip_info(netif, &ip)); + + ESP_LOGI(TAG, "- IPv4 address: " IPSTR ",", IP2STR(&ip.ip)); +#endif +#if CONFIG_EXAMPLE_CONNECT_IPV6 + esp_ip6_addr_t ip6[MAX_IP6_ADDRS_PER_NETIF]; + int ip6_addrs = esp_netif_get_all_ip6(netif, ip6); + for (int j = 0; j < ip6_addrs; ++j) { + esp_ip6_addr_type_t ipv6_type = esp_netif_ip6_get_addr_type(&(ip6[j])); + ESP_LOGI(TAG, "- IPv6 address: " IPV6STR ", type: %s", IPV62STR(ip6[j]), example_ipv6_addr_types_to_str[ipv6_type]); + } +#endif + } + } + return ESP_OK; +} + +void example_print_all_netif_ips(const char *prefix) +{ + // Print all IPs in TCPIP context to avoid potential races of removing/adding netifs when iterating over the list + esp_netif_tcpip_exec(print_all_ips_tcpip, (void*) prefix); +} + + +esp_err_t example_connect(void) +{ +#if CONFIG_EXAMPLE_CONNECT_ETHERNET + if (example_ethernet_connect() != ESP_OK) { + return ESP_FAIL; + } + ESP_ERROR_CHECK(esp_register_shutdown_handler(&example_ethernet_shutdown)); +#endif +#if CONFIG_EXAMPLE_CONNECT_WIFI + if (example_wifi_connect() != ESP_OK) { + return ESP_FAIL; + } + ESP_ERROR_CHECK(esp_register_shutdown_handler(&example_wifi_shutdown)); +#endif +#if CONFIG_EXAMPLE_CONNECT_THREAD + if (example_thread_connect() != ESP_OK) { + return ESP_FAIL; + } + ESP_ERROR_CHECK(esp_register_shutdown_handler(&example_thread_shutdown)); +#endif +#if CONFIG_EXAMPLE_CONNECT_PPP + if (example_ppp_connect() != ESP_OK) { + return ESP_FAIL; + } + ESP_ERROR_CHECK(esp_register_shutdown_handler(&example_ppp_shutdown)); +#endif + +#if CONFIG_EXAMPLE_CONNECT_ETHERNET + example_print_all_netif_ips(EXAMPLE_NETIF_DESC_ETH); +#endif + +#if CONFIG_EXAMPLE_CONNECT_WIFI + example_print_all_netif_ips(EXAMPLE_NETIF_DESC_STA); +#endif + +#if CONFIG_EXAMPLE_CONNECT_THREAD + example_print_all_netif_ips(EXAMPLE_NETIF_DESC_THREAD); +#endif + +#if CONFIG_EXAMPLE_CONNECT_PPP + example_print_all_netif_ips(EXAMPLE_NETIF_DESC_PPP); +#endif + + return ESP_OK; +} + + +esp_err_t example_disconnect(void) +{ +#if CONFIG_EXAMPLE_CONNECT_ETHERNET + example_ethernet_shutdown(); + ESP_ERROR_CHECK(esp_unregister_shutdown_handler(&example_ethernet_shutdown)); +#endif +#if CONFIG_EXAMPLE_CONNECT_WIFI + example_wifi_shutdown(); + ESP_ERROR_CHECK(esp_unregister_shutdown_handler(&example_wifi_shutdown)); +#endif + return ESP_OK; +} diff --git a/components/protocol_examples_common/connect.c:Zone.Identifier b/components/protocol_examples_common/connect.c:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x +#include "protocol_examples_common.h" +#include "example_common_private.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_console.h" +#include "argtable3/argtable3.h" + + +static const char *TAG = "example_console"; + +typedef struct { + struct arg_str *ssid; + struct arg_str *password; + struct arg_int *channel; + struct arg_end *end; +} wifi_connect_args_t; +static wifi_connect_args_t connect_args; + +static int cmd_do_wifi_connect(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **) &connect_args); + + if (nerrors != 0) { + arg_print_errors(stderr, connect_args.end, argv[0]); + return 1; + } + + wifi_config_t wifi_config = { + .sta = { + .scan_method = WIFI_ALL_CHANNEL_SCAN, + .sort_method = WIFI_CONNECT_AP_BY_SIGNAL, + }, + }; + if (connect_args.channel->count > 0) { + wifi_config.sta.channel = (uint8_t)(connect_args.channel->ival[0]); + } + const char *ssid = connect_args.ssid->sval[0]; + const char *pass = connect_args.password->sval[0]; + strlcpy((char *) wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid)); + if (pass) { + strlcpy((char *) wifi_config.sta.password, pass, sizeof(wifi_config.sta.password)); + } + example_wifi_sta_do_connect(wifi_config, false); + return 0; +} + +static int cmd_do_wifi_disconnect(int argc, char **argv) +{ + example_wifi_sta_do_disconnect(); + return 0; +} + +void example_register_wifi_connect_commands(void) +{ + ESP_LOGI(TAG, "Registering WiFi connect commands."); + example_wifi_start(); + + connect_args.ssid = arg_str1(NULL, NULL, "", "SSID of AP"); + connect_args.password = arg_str0(NULL, NULL, "", "password of AP"); + connect_args.channel = arg_int0("n", "channel", "", "channel of AP"); + connect_args.end = arg_end(2); + const esp_console_cmd_t wifi_connect_cmd = { + .command = "wifi_connect", + .help = "WiFi is station mode, join specified soft-AP", + .hint = NULL, + .func = &cmd_do_wifi_connect, + .argtable = &connect_args + }; + ESP_ERROR_CHECK( esp_console_cmd_register(&wifi_connect_cmd) ); + + + const esp_console_cmd_t wifi_disconnect_cmd = { + .command = "wifi_disconnect", + .help = "Do wifi disconnect", + .hint = NULL, + .func = &cmd_do_wifi_disconnect, + }; + ESP_ERROR_CHECK( esp_console_cmd_register(&wifi_disconnect_cmd) ); +} diff --git a/components/protocol_examples_common/console_cmd.c:Zone.Identifier b/components/protocol_examples_common/console_cmd.c:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x +#include "protocol_examples_common.h" +#include "example_common_private.h" +#include "esp_event.h" +#include "esp_eth.h" +#if CONFIG_ETH_USE_SPI_ETHERNET +#include "driver/spi_master.h" +#endif // CONFIG_ETH_USE_SPI_ETHERNET +#include "esp_log.h" +#include "esp_mac.h" +#include "driver/gpio.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" + + +static const char *TAG = "ethernet_connect"; +static SemaphoreHandle_t s_semph_get_ip_addrs = NULL; +#if CONFIG_EXAMPLE_CONNECT_IPV6 +static SemaphoreHandle_t s_semph_get_ip6_addrs = NULL; +#endif + +static esp_netif_t *eth_start(void); +static void eth_stop(void); + + +/** Event handler for Ethernet events */ + +static void eth_on_got_ip(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; + if (!example_is_our_netif(EXAMPLE_NETIF_DESC_ETH, event->esp_netif)) { + return; + } + ESP_LOGI(TAG, "Got IPv4 event: Interface \"%s\" address: " IPSTR, esp_netif_get_desc(event->esp_netif), IP2STR(&event->ip_info.ip)); + xSemaphoreGive(s_semph_get_ip_addrs); +} + +#if CONFIG_EXAMPLE_CONNECT_IPV6 + +static void eth_on_got_ipv6(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + ip_event_got_ip6_t *event = (ip_event_got_ip6_t *)event_data; + if (!example_is_our_netif(EXAMPLE_NETIF_DESC_ETH, event->esp_netif)) { + return; + } + esp_ip6_addr_type_t ipv6_type = esp_netif_ip6_get_addr_type(&event->ip6_info.ip); + ESP_LOGI(TAG, "Got IPv6 event: Interface \"%s\" address: " IPV6STR ", type: %s", esp_netif_get_desc(event->esp_netif), + IPV62STR(event->ip6_info.ip), example_ipv6_addr_types_to_str[ipv6_type]); + if (ipv6_type == EXAMPLE_CONNECT_PREFERRED_IPV6_TYPE) { + xSemaphoreGive(s_semph_get_ip6_addrs); + } +} + +static void on_eth_event(void *esp_netif, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + switch (event_id) { + case ETHERNET_EVENT_CONNECTED: + ESP_LOGI(TAG, "Ethernet Link Up"); + ESP_ERROR_CHECK(esp_netif_create_ip6_linklocal(esp_netif)); + break; + default: + break; + } +} + +#endif // CONFIG_EXAMPLE_CONNECT_IPV6 + +static esp_eth_handle_t s_eth_handle = NULL; +static esp_eth_mac_t *s_mac = NULL; +static esp_eth_phy_t *s_phy = NULL; +static esp_eth_netif_glue_handle_t s_eth_glue = NULL; + +static esp_netif_t *eth_start(void) +{ + esp_netif_inherent_config_t esp_netif_config = ESP_NETIF_INHERENT_DEFAULT_ETH(); + // Warning: the interface desc is used in tests to capture actual connection details (IP, gw, mask) + esp_netif_config.if_desc = EXAMPLE_NETIF_DESC_ETH; + esp_netif_config.route_prio = 64; + esp_netif_config_t netif_config = { + .base = &esp_netif_config, + .stack = ESP_NETIF_NETSTACK_DEFAULT_ETH + }; + esp_netif_t *netif = esp_netif_new(&netif_config); + assert(netif); + + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + mac_config.rx_task_stack_size = CONFIG_EXAMPLE_ETHERNET_EMAC_TASK_STACK_SIZE; + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + phy_config.phy_addr = CONFIG_EXAMPLE_ETH_PHY_ADDR; + phy_config.reset_gpio_num = CONFIG_EXAMPLE_ETH_PHY_RST_GPIO; +#if CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET + eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); + esp32_emac_config.smi_gpio.mdc_num = CONFIG_EXAMPLE_ETH_MDC_GPIO; + esp32_emac_config.smi_gpio.mdio_num = CONFIG_EXAMPLE_ETH_MDIO_GPIO; + s_mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); +#if CONFIG_EXAMPLE_ETH_PHY_GENERIC + s_phy = esp_eth_phy_new_generic(&phy_config); +#elif CONFIG_EXAMPLE_ETH_PHY_IP101 + s_phy = esp_eth_phy_new_ip101(&phy_config); +#elif CONFIG_EXAMPLE_ETH_PHY_RTL8201 + s_phy = esp_eth_phy_new_rtl8201(&phy_config); +#elif CONFIG_EXAMPLE_ETH_PHY_LAN87XX + s_phy = esp_eth_phy_new_lan87xx(&phy_config); +#elif CONFIG_EXAMPLE_ETH_PHY_DP83848 + s_phy = esp_eth_phy_new_dp83848(&phy_config); +#elif CONFIG_EXAMPLE_ETH_PHY_KSZ80XX + s_phy = esp_eth_phy_new_ksz80xx(&phy_config); +#endif +#elif CONFIG_EXAMPLE_USE_SPI_ETHERNET + gpio_install_isr_service(0); + spi_bus_config_t buscfg = { + .miso_io_num = CONFIG_EXAMPLE_ETH_SPI_MISO_GPIO, + .mosi_io_num = CONFIG_EXAMPLE_ETH_SPI_MOSI_GPIO, + .sclk_io_num = CONFIG_EXAMPLE_ETH_SPI_SCLK_GPIO, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + }; + ESP_ERROR_CHECK(spi_bus_initialize(CONFIG_EXAMPLE_ETH_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO)); + spi_device_interface_config_t spi_devcfg = { + .mode = 0, + .clock_speed_hz = CONFIG_EXAMPLE_ETH_SPI_CLOCK_MHZ * 1000 * 1000, + .spics_io_num = CONFIG_EXAMPLE_ETH_SPI_CS_GPIO, + .queue_size = 20 + }; +#if CONFIG_EXAMPLE_USE_DM9051 + /* dm9051 ethernet driver is based on spi driver */ + eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(CONFIG_EXAMPLE_ETH_SPI_HOST, &spi_devcfg); + dm9051_config.int_gpio_num = CONFIG_EXAMPLE_ETH_SPI_INT_GPIO; + s_mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config); + s_phy = esp_eth_phy_new_dm9051(&phy_config); +#elif CONFIG_EXAMPLE_USE_W5500 + /* w5500 ethernet driver is based on spi driver */ + eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(CONFIG_EXAMPLE_ETH_SPI_HOST, &spi_devcfg); + w5500_config.int_gpio_num = CONFIG_EXAMPLE_ETH_SPI_INT_GPIO; + s_mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); + s_phy = esp_eth_phy_new_w5500(&phy_config); +#endif +#elif CONFIG_EXAMPLE_USE_OPENETH + phy_config.autonego_timeout_ms = 100; + s_mac = esp_eth_mac_new_openeth(&mac_config); + s_phy = esp_eth_phy_new_dp83848(&phy_config); +#endif + + // Install Ethernet driver + esp_eth_config_t config = ETH_DEFAULT_CONFIG(s_mac, s_phy); + ESP_ERROR_CHECK(esp_eth_driver_install(&config, &s_eth_handle)); + +#if CONFIG_EXAMPLE_USE_SPI_ETHERNET + /* The SPI Ethernet module might doesn't have a burned factory MAC address, we cat to set it manually. + We set the ESP_MAC_ETH mac address as the default, if you want to use ESP_MAC_EFUSE_CUSTOM mac address, please enable the + configuration: `ESP_MAC_USE_CUSTOM_MAC_AS_BASE_MAC` + */ + uint8_t eth_mac[6] = {0}; + ESP_ERROR_CHECK(esp_read_mac(eth_mac, ESP_MAC_ETH)); + ESP_ERROR_CHECK(esp_eth_ioctl(s_eth_handle, ETH_CMD_S_MAC_ADDR, eth_mac)); +#endif // CONFIG_EXAMPLE_USE_SPI_ETHERNET + + // combine driver with netif + s_eth_glue = esp_eth_new_netif_glue(s_eth_handle); + esp_netif_attach(netif, s_eth_glue); + + // Register user defined event handlers + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, ð_on_got_ip, NULL)); +#ifdef CONFIG_EXAMPLE_CONNECT_IPV6 + ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ETHERNET_EVENT_CONNECTED, &on_eth_event, netif)); + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, ð_on_got_ipv6, NULL)); +#endif + + esp_eth_start(s_eth_handle); + return netif; +} + +static void eth_stop(void) +{ + esp_netif_t *eth_netif = get_example_netif_from_desc(EXAMPLE_NETIF_DESC_ETH); + ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_ETH_GOT_IP, ð_on_got_ip)); +#if CONFIG_EXAMPLE_CONNECT_IPV6 + ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_GOT_IP6, ð_on_got_ipv6)); + ESP_ERROR_CHECK(esp_event_handler_unregister(ETH_EVENT, ETHERNET_EVENT_CONNECTED, &on_eth_event)); +#endif + ESP_ERROR_CHECK(esp_eth_stop(s_eth_handle)); + ESP_ERROR_CHECK(esp_eth_del_netif_glue(s_eth_glue)); + ESP_ERROR_CHECK(esp_eth_driver_uninstall(s_eth_handle)); + s_eth_handle = NULL; + ESP_ERROR_CHECK(s_phy->del(s_phy)); + ESP_ERROR_CHECK(s_mac->del(s_mac)); + + esp_netif_destroy(eth_netif); +} + +esp_eth_handle_t get_example_eth_handle(void) +{ + return s_eth_handle; +} + +/* tear down connection, release resources */ +void example_ethernet_shutdown(void) +{ + if (s_semph_get_ip_addrs == NULL) { + return; + } + vSemaphoreDelete(s_semph_get_ip_addrs); + s_semph_get_ip_addrs = NULL; +#if CONFIG_EXAMPLE_CONNECT_IPV6 + vSemaphoreDelete(s_semph_get_ip6_addrs); + s_semph_get_ip6_addrs = NULL; +#endif + eth_stop(); +} + +esp_err_t example_ethernet_connect(void) +{ +#if CONFIG_EXAMPLE_CONNECT_IPV4 + s_semph_get_ip_addrs = xSemaphoreCreateBinary(); + if (s_semph_get_ip_addrs == NULL) { + return ESP_ERR_NO_MEM; + } +#endif +#if CONFIG_EXAMPLE_CONNECT_IPV6 + s_semph_get_ip6_addrs = xSemaphoreCreateBinary(); + if (s_semph_get_ip6_addrs == NULL) { + vSemaphoreDelete(s_semph_get_ip_addrs); + return ESP_ERR_NO_MEM; + } +#endif + eth_start(); + ESP_LOGI(TAG, "Waiting for IP(s)."); +#if CONFIG_EXAMPLE_CONNECT_IPV4 + xSemaphoreTake(s_semph_get_ip_addrs, portMAX_DELAY); +#endif +#if CONFIG_EXAMPLE_CONNECT_IPV6 + xSemaphoreTake(s_semph_get_ip6_addrs, portMAX_DELAY); +#endif + return ESP_OK; +} diff --git a/components/protocol_examples_common/eth_connect.c:Zone.Identifier b/components/protocol_examples_common/eth_connect.c:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Read and evaluate IP address from stdin + * + * This API reads stdin and parses the input address using getaddrinfo() + * to fill in struct sockaddr_storage (for both IPv4 and IPv6) used to open + * a socket. IP protocol is guessed from the IP address string. + * + * @param[in] port port number of expected connection + * @param[in] sock_type expected protocol: SOCK_STREAM or SOCK_DGRAM + * @param[out] ip_protocol resultant IP protocol: IPPROTO_IP or IPPROTO_IP6 + * @param[out] addr_family resultant address family: AF_INET or AF_INET6 + * @param[out] dest_addr sockaddr_storage structure (for both IPv4 and IPv6) + * @return ESP_OK on success, ESP_FAIL otherwise + */ +esp_err_t get_addr_from_stdin(int port, int sock_type, + int *ip_protocol, + int *addr_family, + struct sockaddr_storage *dest_addr); + +#ifdef __cplusplus +} +#endif diff --git a/components/protocol_examples_common/include/addr_from_stdin.h:Zone.Identifier b/components/protocol_examples_common/include/addr_from_stdin.h:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2xdl#JyUFr831@K2xdl#JyUFr831@K2x + +#include + +#ifdef CONFIG_OPENTHREAD_RADIO_NATIVE +#define ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG() \ + { \ + .radio_mode = RADIO_MODE_NATIVE, \ + } + +#elif defined(CONFIG_OPENTHREAD_RADIO_SPINEL_UART) +#define ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG() \ + { \ + .radio_mode = RADIO_MODE_UART_RCP, \ + .radio_uart_config = \ + { \ + .port = CONFIG_EXAMPLE_THREAD_UART_PORT, \ + .uart_config = \ + { \ + .baud_rate = CONFIG_EXAMPLE_THREAD_UART_BAUD, \ + .data_bits = UART_DATA_8_BITS, \ + .parity = UART_PARITY_DISABLE, \ + .stop_bits = UART_STOP_BITS_1, \ + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, \ + .rx_flow_ctrl_thresh = 0, \ + .source_clk = UART_SCLK_DEFAULT, \ + }, \ + .rx_pin = CONFIG_EXAMPLE_THREAD_UART_RX_PIN, \ + .tx_pin = CONFIG_EXAMPLE_THREAD_UART_TX_PIN, \ + }, \ + } +#elif defined(CONFIG_OPENTHREAD_RADIO_SPINEL_SPI) +#define ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG() \ + { \ + .radio_mode = RADIO_MODE_SPI_RCP, \ + .radio_spi_config = \ + { \ + .host_device = SPI2_HOST, \ + .dma_channel = 2, \ + .spi_interface = \ + { \ + .mosi_io_num = CONFIG_EXAMPLE_THREAD_SPI_MOSI_PIN, \ + .miso_io_num = CONFIG_EXAMPLE_THREAD_SPI_MISO_PIN, \ + .sclk_io_num = CONFIG_EXAMPLE_THREAD_SPI_SCLK_PIN, \ + .quadwp_io_num = -1, \ + .quadhd_io_num = -1, \ + }, \ + .spi_device = \ + { \ + .cs_ena_pretrans = 2, \ + .input_delay_ns = 100, \ + .mode = 0, \ + .clock_speed_hz = 2500 * 1000, \ + .spics_io_num = CONFIG_EXAMPLE_THREAD_SPI_CS_PIN, \ + .queue_size = 5, \ + }, \ + .intr_pin = CONFIG_EXAMPLE_THREAD_SPI_INTR_PIN, \ + }, \ + } +#else +#define ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG() \ + { \ + .radio_mode = RADIO_MODE_TREL, \ + } +#endif + +#if CONFIG_OPENTHREAD_CONSOLE_TYPE_UART +#define ESP_OPENTHREAD_DEFAULT_HOST_CONFIG() \ + { \ + .host_connection_mode = HOST_CONNECTION_MODE_CLI_UART, \ + .host_uart_config = \ + { \ + .port = 0, \ + .uart_config = \ + { \ + .baud_rate = 115200, \ + .data_bits = UART_DATA_8_BITS, \ + .parity = UART_PARITY_DISABLE, \ + .stop_bits = UART_STOP_BITS_1, \ + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, \ + .rx_flow_ctrl_thresh = 0, \ + .source_clk = UART_SCLK_DEFAULT, \ + }, \ + .rx_pin = UART_PIN_NO_CHANGE, \ + .tx_pin = UART_PIN_NO_CHANGE, \ + }, \ + } +#elif CONFIG_OPENTHREAD_CONSOLE_TYPE_USB_SERIAL_JTAG +#define ESP_OPENTHREAD_DEFAULT_HOST_CONFIG() \ + { \ + .host_connection_mode = HOST_CONNECTION_MODE_CLI_USB, \ + .host_usb_config = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT(), \ + } +#else +#define ESP_OPENTHREAD_DEFAULT_HOST_CONFIG() \ + { \ + .host_connection_mode = HOST_CONNECTION_MODE_NONE, \ + } +#endif + +#define ESP_OPENTHREAD_DEFAULT_PORT_CONFIG() \ + { \ + .storage_partition_name = "nvs", \ + .netif_queue_size = 10, \ + .task_queue_size = 10, \ + } diff --git a/components/protocol_examples_common/include/protocol_examples_thread_config.h:Zone.Identifier b/components/protocol_examples_common/include/protocol_examples_thread_config.h:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * @brief Encode an URI + * + * @param dest a destination memory location + * @param src the source string + * @param len the length of the source string + * @return uint32_t the count of escaped characters + * + * @note Please allocate the destination buffer keeping in mind that encoding a + * special character will take up 3 bytes (for '%' and two hex digits). + * In the worst-case scenario, the destination buffer will have to be 3 times + * that of the source string. + */ +uint32_t example_uri_encode(char *dest, const char *src, size_t len); + +/** + * @brief Decode an URI + * + * @param dest a destination memory location + * @param src the source string + * @param len the length of the source string + * + * @note Please allocate the destination buffer keeping in mind that a decoded + * special character will take up 2 less bytes than its encoded form. + * In the worst-case scenario, the destination buffer will have to be + * the same size that of the source string. + */ +void example_uri_decode(char *dest, const char *src, size_t len); + +#ifdef __cplusplus +} +#endif diff --git a/components/protocol_examples_common/include/protocol_examples_utils.h:Zone.Identifier b/components/protocol_examples_common/include/protocol_examples_utils.h:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x +#include +#include "sdkconfig.h" +#include "protocol_examples_common.h" +#include "example_common_private.h" + +#if CONFIG_EXAMPLE_CONNECT_PPP +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_netif_ppp.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#if CONFIG_EXAMPLE_CONNECT_PPP_DEVICE_USB +#include "tinyusb.h" +#include "tusb_cdc_acm.h" + +static int s_itf; +static uint8_t buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE]; + +#else // DEVICE is UART + +#include "driver/uart.h" +#define BUF_SIZE (1024) +static bool s_stop_task = false; + +#endif // CONNECT_PPP_DEVICE + + +static const char *TAG = "example_connect_ppp"; +static int s_retry_num = 0; +static EventGroupHandle_t s_event_group = NULL; +static esp_netif_t *s_netif; +static const int GOT_IPV4 = BIT0; +static const int CONNECTION_FAILED = BIT1; +#if CONFIG_EXAMPLE_CONNECT_IPV6 +static const int GOT_IPV6 = BIT2; +#define CONNECT_BITS (GOT_IPV4|GOT_IPV6|CONNECTION_FAILED) +#else +#define CONNECT_BITS (GOT_IPV4|CONNECTION_FAILED) +#endif + +static esp_err_t transmit(void *h, void *buffer, size_t len) +{ + ESP_LOG_BUFFER_HEXDUMP(TAG, buffer, len, ESP_LOG_VERBOSE); +#if CONFIG_EXAMPLE_CONNECT_PPP_DEVICE_USB + tinyusb_cdcacm_write_queue(s_itf, buffer, len); + tinyusb_cdcacm_write_flush(s_itf, 0); +#else // DEVICE_UART + uart_write_bytes(UART_NUM_1, buffer, len); +#endif // CONNECT_PPP_DEVICE + return ESP_OK; +} + +static esp_netif_driver_ifconfig_t driver_cfg = { + .handle = (void *)1, // singleton driver, just to != NULL + .transmit = transmit, +}; +const esp_netif_driver_ifconfig_t *ppp_driver_cfg = &driver_cfg; + +static void on_ip_event(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + + if (event_id == IP_EVENT_PPP_GOT_IP) { + ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; + if (!example_is_our_netif(EXAMPLE_NETIF_DESC_PPP, event->esp_netif)) { + return; + } + esp_netif_t *netif = event->esp_netif; + esp_netif_dns_info_t dns_info; + ESP_LOGI(TAG, "Got IPv4 event: Interface \"%s\" address: " IPSTR, esp_netif_get_desc(event->esp_netif), IP2STR(&event->ip_info.ip)); + esp_netif_get_dns_info(netif, ESP_NETIF_DNS_MAIN, &dns_info); + ESP_LOGI(TAG, "Main DNS server : " IPSTR, IP2STR(&dns_info.ip.u_addr.ip4)); + xEventGroupSetBits(s_event_group, GOT_IPV4); +#if CONFIG_EXAMPLE_CONNECT_IPV6 + } else if (event_id == IP_EVENT_GOT_IP6) { + ip_event_got_ip6_t *event = (ip_event_got_ip6_t *)event_data; + if (!example_is_our_netif(EXAMPLE_NETIF_DESC_PPP, event->esp_netif)) { + return; + } + esp_ip6_addr_type_t ipv6_type = esp_netif_ip6_get_addr_type(&event->ip6_info.ip); + ESP_LOGI(TAG, "Got IPv6 event: Interface \"%s\" address: " IPV6STR ", type: %s", esp_netif_get_desc(event->esp_netif), + IPV62STR(event->ip6_info.ip), example_ipv6_addr_types_to_str[ipv6_type]); + if (ipv6_type == EXAMPLE_CONNECT_PREFERRED_IPV6_TYPE) { + xEventGroupSetBits(s_event_group, GOT_IPV6); + } +#endif + } else if (event_id == IP_EVENT_PPP_LOST_IP) { + ESP_LOGI(TAG, "Disconnect from PPP Server"); + s_retry_num++; + if (s_retry_num > CONFIG_EXAMPLE_PPP_CONN_MAX_RETRY) { + ESP_LOGE(TAG, "PPP Connection failed %d times, stop reconnecting.", s_retry_num); + xEventGroupSetBits(s_event_group, CONNECTION_FAILED); + } else { + ESP_LOGI(TAG, "PPP Connection failed %d times, try to reconnect.", s_retry_num); + esp_netif_action_start(s_netif, 0, 0, 0); + esp_netif_action_connected(s_netif, 0, 0, 0); + } + + } +} + +#if CONFIG_EXAMPLE_CONNECT_PPP_DEVICE_USB +static void cdc_rx_callback(int itf, cdcacm_event_t *event) +{ + size_t rx_size = 0; + if (itf != s_itf) { + // Not our channel + return; + } + esp_err_t ret = tinyusb_cdcacm_read(itf, buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size); + if (ret == ESP_OK) { + ESP_LOG_BUFFER_HEXDUMP(TAG, buf, rx_size, ESP_LOG_VERBOSE); + // pass the received data to the network interface + esp_netif_receive(s_netif, buf, rx_size, NULL); + } else { + ESP_LOGE(TAG, "Read error"); + } +} + +static void line_state_changed(int itf, cdcacm_event_t *event) +{ + s_itf = itf; // use this channel for the netif communication + ESP_LOGI(TAG, "Line state changed on channel %d", itf); +} +#else // DEVICE is UART + +static void ppp_task(void *args) +{ + uart_config_t uart_config = {}; + uart_config.baud_rate = CONFIG_EXAMPLE_CONNECT_UART_BAUDRATE; + uart_config.data_bits = UART_DATA_8_BITS; + uart_config.parity = UART_PARITY_DISABLE; + uart_config.stop_bits = UART_STOP_BITS_1; + uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; + uart_config.source_clk = UART_SCLK_DEFAULT; + + QueueHandle_t event_queue; + ESP_ERROR_CHECK(uart_driver_install(UART_NUM_1, BUF_SIZE, 0, 16, &event_queue, 0)); + ESP_ERROR_CHECK(uart_param_config(UART_NUM_1, &uart_config)); + ESP_ERROR_CHECK(uart_set_pin(UART_NUM_1, CONFIG_EXAMPLE_CONNECT_UART_TX_PIN, CONFIG_EXAMPLE_CONNECT_UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); + ESP_ERROR_CHECK(uart_set_rx_timeout(UART_NUM_1, 1)); + + char *buffer = (char*)malloc(BUF_SIZE); + uart_event_t event; + esp_event_handler_register(IP_EVENT, IP_EVENT_PPP_GOT_IP, esp_netif_action_connected, s_netif); + esp_netif_action_start(s_netif, 0, 0, 0); + esp_netif_action_connected(s_netif, 0, 0, 0); + while (!s_stop_task) { + xQueueReceive(event_queue, &event, pdMS_TO_TICKS(1000)); + if (event.type == UART_DATA) { + size_t len; + uart_get_buffered_data_len(UART_NUM_1, &len); + if (len) { + len = uart_read_bytes(UART_NUM_1, buffer, BUF_SIZE, 0); + ESP_LOG_BUFFER_HEXDUMP(TAG, buffer, len, ESP_LOG_VERBOSE); + esp_netif_receive(s_netif, buffer, len, NULL); + } + } else { + ESP_LOGW(TAG, "Received UART event: %d", event.type); + } + } + free(buffer); + vTaskDelete(NULL); +} + +#endif // CONNECT_PPP_DEVICE + +esp_err_t example_ppp_connect(void) +{ + ESP_LOGI(TAG, "Start example_connect."); + +#if CONFIG_EXAMPLE_CONNECT_PPP_DEVICE_USB + ESP_LOGI(TAG, "USB initialization"); + const tinyusb_config_t tusb_cfg = { + .device_descriptor = NULL, + .string_descriptor = NULL, + .external_phy = false, + .configuration_descriptor = NULL, + }; + + ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg)); + + tinyusb_config_cdcacm_t acm_cfg = { + .usb_dev = TINYUSB_USBDEV_0, + .cdc_port = TINYUSB_CDC_ACM_0, + .callback_rx = &cdc_rx_callback, + .callback_rx_wanted_char = NULL, + .callback_line_state_changed = NULL, + .callback_line_coding_changed = NULL + }; + + ESP_ERROR_CHECK(tusb_cdc_acm_init(&acm_cfg)); + /* the second way to register a callback */ + ESP_ERROR_CHECK(tinyusb_cdcacm_register_callback( + TINYUSB_CDC_ACM_0, + CDC_EVENT_LINE_STATE_CHANGED, + &line_state_changed)); +#endif // CONFIG_EXAMPLE_CONNECT_PPP_DEVICE_USB + + s_event_group = xEventGroupCreate(); + + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, on_ip_event, NULL)); + + esp_netif_inherent_config_t base_netif_cfg = ESP_NETIF_INHERENT_DEFAULT_PPP(); + base_netif_cfg.if_desc = EXAMPLE_NETIF_DESC_PPP; + esp_netif_config_t netif_ppp_config = { .base = &base_netif_cfg, + .driver = ppp_driver_cfg, + .stack = ESP_NETIF_NETSTACK_DEFAULT_PPP + }; + + s_netif = esp_netif_new(&netif_ppp_config); + assert(s_netif); +#if CONFIG_EXAMPLE_CONNECT_PPP_DEVICE_USB + esp_netif_action_start(s_netif, 0, 0, 0); + esp_netif_action_connected(s_netif, 0, 0, 0); +#else // DEVICE is UART + s_stop_task = false; + if (xTaskCreate(ppp_task, "ppp connect", 4096, NULL, 5, NULL) != pdTRUE) { + ESP_LOGE(TAG, "Failed to create a ppp connection task"); + return ESP_FAIL; + } +#endif // CONNECT_PPP_DEVICE + + ESP_LOGI(TAG, "Waiting for IP address"); + EventBits_t bits = xEventGroupWaitBits(s_event_group, CONNECT_BITS, pdFALSE, pdFALSE, portMAX_DELAY); + if (bits & CONNECTION_FAILED) { + ESP_LOGE(TAG, "Connection failed!"); + return ESP_FAIL; + } + ESP_LOGI(TAG, "Connected!"); + + return ESP_OK; +} + +void example_ppp_shutdown(void) +{ + ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, ESP_EVENT_ANY_ID, on_ip_event)); +#if CONFIG_EXAMPLE_CONNECT_PPP_DEVICE_UART + s_stop_task = true; + vTaskDelay(pdMS_TO_TICKS(1000)); // wait for the ppp task to stop +#endif + + esp_netif_action_disconnected(s_netif, 0, 0, 0); + + vEventGroupDelete(s_event_group); + esp_netif_action_stop(s_netif, 0, 0, 0); + esp_netif_destroy(s_netif); + s_netif = NULL; + s_event_group = NULL; +} + +#endif // CONFIG_EXAMPLE_CONNECT_PPP diff --git a/components/protocol_examples_common/ppp_connect.c:Zone.Identifier b/components/protocol_examples_common/ppp_connect.c:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x +#include +#include +#include + +#include "protocol_examples_utils.h" + +/* Type of Escape algorithms to be used */ +#define NGX_ESCAPE_URI (0) +#define NGX_ESCAPE_ARGS (1) +#define NGX_ESCAPE_URI_COMPONENT (2) +#define NGX_ESCAPE_HTML (3) +#define NGX_ESCAPE_REFRESH (4) +#define NGX_ESCAPE_MEMCACHED (5) +#define NGX_ESCAPE_MAIL_AUTH (6) + +/* Type of Unescape algorithms to be used */ +#define NGX_UNESCAPE_URI (1) +#define NGX_UNESCAPE_REDIRECT (2) + + +uintptr_t ngx_escape_uri(u_char *dst, u_char *src, size_t size, unsigned int type) +{ + unsigned int n; + uint32_t *escape; + static u_char hex[] = "0123456789ABCDEF"; + + /* + * Per RFC 3986 only the following chars are allowed in URIs unescaped: + * + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + * + * And "%" can appear as a part of escaping itself. The following + * characters are not allowed and need to be escaped: %00-%1F, %7F-%FF, + * " ", """, "<", ">", "\", "^", "`", "{", "|", "}". + */ + + /* " ", "#", "%", "?", not allowed */ + + static uint32_t uri[] = { + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + + /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ + 0xd000002d, /* 1101 0000 0000 0000 0000 0000 0010 1101 */ + + /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ + 0x50000000, /* 0101 0000 0000 0000 0000 0000 0000 0000 */ + + /* ~}| {zyx wvut srqp onml kjih gfed cba` */ + 0xb8000001, /* 1011 1000 0000 0000 0000 0000 0000 0001 */ + + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + }; + + /* " ", "#", "%", "&", "+", ";", "?", not allowed */ + + static uint32_t args[] = { + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + + /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ + 0xd800086d, /* 1101 1000 0000 0000 0000 1000 0110 1101 */ + + /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ + 0x50000000, /* 0101 0000 0000 0000 0000 0000 0000 0000 */ + + /* ~}| {zyx wvut srqp onml kjih gfed cba` */ + 0xb8000001, /* 1011 1000 0000 0000 0000 0000 0000 0001 */ + + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + }; + + /* not ALPHA, DIGIT, "-", ".", "_", "~" */ + + static uint32_t uri_component[] = { + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + + /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ + 0xfc009fff, /* 1111 1100 0000 0000 1001 1111 1111 1111 */ + + /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ + 0x78000001, /* 0111 1000 0000 0000 0000 0000 0000 0001 */ + + /* ~}| {zyx wvut srqp onml kjih gfed cba` */ + 0xb8000001, /* 1011 1000 0000 0000 0000 0000 0000 0001 */ + + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + }; + + /* " ", "#", """, "%", "'", not allowed */ + + static uint32_t html[] = { + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + + /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ + 0x500000ad, /* 0101 0000 0000 0000 0000 0000 1010 1101 */ + + /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ + 0x50000000, /* 0101 0000 0000 0000 0000 0000 0000 0000 */ + + /* ~}| {zyx wvut srqp onml kjih gfed cba` */ + 0xb8000001, /* 1011 1000 0000 0000 0000 0000 0000 0001 */ + + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + }; + + /* " ", """, "'", not allowed */ + + static uint32_t refresh[] = { + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + + /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ + 0x50000085, /* 0101 0000 0000 0000 0000 0000 1000 0101 */ + + /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ + 0x50000000, /* 0101 0000 0000 0000 0000 0000 0000 0000 */ + + /* ~}| {zyx wvut srqp onml kjih gfed cba` */ + 0xd8000001, /* 1011 1000 0000 0000 0000 0000 0000 0001 */ + + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + }; + + /* " ", "%", %00-%1F */ + + static uint32_t memcached[] = { + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + + /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ + 0x00000021, /* 0000 0000 0000 0000 0000 0000 0010 0001 */ + + /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + + /* ~}| {zyx wvut srqp onml kjih gfed cba` */ + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + }; + + /* mail_auth is the same as memcached */ + + static uint32_t *map[] = + { uri, args, uri_component, html, refresh, memcached, memcached }; + + + escape = map[type]; + + if (dst == NULL) { + + /* find the number of the characters to be escaped */ + + n = 0; + + while (size) { + if (escape[*src >> 5] & (1U << (*src & 0x1f))) { + n++; + } + src++; + size--; + } + + return (uintptr_t) n; + } + + while (size) { + if (escape[*src >> 5] & (1U << (*src & 0x1f))) { + *dst++ = '%'; + *dst++ = hex[*src >> 4]; + *dst++ = hex[*src & 0xf]; + src++; + + } else { + *dst++ = *src++; + } + size--; + } + + return (uintptr_t) dst; +} + + +void ngx_unescape_uri(u_char **dst, u_char **src, size_t size, unsigned int type) +{ + u_char *d, *s, ch, c, decoded; + enum { + sw_usual = 0, + sw_quoted, + sw_quoted_second + } state; + + d = *dst; + s = *src; + + state = 0; + decoded = 0; + + while (size--) { + + ch = *s++; + + switch (state) { + case sw_usual: + if (ch == '?' + && (type & (NGX_UNESCAPE_URI | NGX_UNESCAPE_REDIRECT))) { + *d++ = ch; + goto done; + } + + if (ch == '%') { + state = sw_quoted; + break; + } + + *d++ = ch; + break; + + case sw_quoted: + + if (ch >= '0' && ch <= '9') { + decoded = (u_char) (ch - '0'); + state = sw_quoted_second; + break; + } + + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'f') { + decoded = (u_char) (c - 'a' + 10); + state = sw_quoted_second; + break; + } + + /* the invalid quoted character */ + + state = sw_usual; + + *d++ = ch; + + break; + + case sw_quoted_second: + + state = sw_usual; + + if (ch >= '0' && ch <= '9') { + ch = (u_char) ((decoded << 4) + (ch - '0')); + + if (type & NGX_UNESCAPE_REDIRECT) { + if (ch > '%' && ch < 0x7f) { + *d++ = ch; + break; + } + + *d++ = '%'; *d++ = *(s - 2); *d++ = *(s - 1); + + break; + } + + *d++ = ch; + + break; + } + + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'f') { + ch = (u_char) ((decoded << 4) + (c - 'a') + 10); + + if (type & NGX_UNESCAPE_URI) { + if (ch == '?') { + *d++ = ch; + goto done; + } + + *d++ = ch; + break; + } + + if (type & NGX_UNESCAPE_REDIRECT) { + if (ch == '?') { + *d++ = ch; + goto done; + } + + if (ch > '%' && ch < 0x7f) { + *d++ = ch; + break; + } + + *d++ = '%'; *d++ = *(s - 2); *d++ = *(s - 1); + break; + } + + *d++ = ch; + + break; + } + + /* the invalid quoted character */ + + break; + } + } + +done: + + *dst = d; + *src = s; +} + + +uint32_t example_uri_encode(char *dest, const char *src, size_t len) +{ + if (!src || !dest) { + return 0; + } + + uintptr_t ret = ngx_escape_uri((unsigned char *)dest, (unsigned char *)src, len, NGX_ESCAPE_URI_COMPONENT); + return (uint32_t)(ret - (uintptr_t)dest); +} + + +void example_uri_decode(char *dest, const char *src, size_t len) +{ + if (!src || !dest) { + return; + } + + unsigned char *src_ptr = (unsigned char *)src; + unsigned char *dst_ptr = (unsigned char *)dest; + ngx_unescape_uri(&dst_ptr, &src_ptr, len, NGX_UNESCAPE_URI); +} diff --git a/components/protocol_examples_common/protocol_examples_utils.c:Zone.Identifier b/components/protocol_examples_common/protocol_examples_utils.c:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2xdl#JyUFr831@K2x + +#include +#include +#include +#include +#include +#include +#include + +static TaskHandle_t s_ot_task_handle = NULL; +static esp_netif_t *s_openthread_netif = NULL; +static SemaphoreHandle_t s_semph_thread_attached = NULL; +static SemaphoreHandle_t s_semph_thread_set_dns_server = NULL; +static const char *TAG = "example_connect"; + +static void thread_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, + void* event_data) +{ + if (event_base == OPENTHREAD_EVENT) { + if (event_id == OPENTHREAD_EVENT_ATTACHED) { + xSemaphoreGive(s_semph_thread_attached); + } else if (event_id == OPENTHREAD_EVENT_SET_DNS_SERVER) { + xSemaphoreGive(s_semph_thread_set_dns_server); + } + } +} + +static void ot_task_worker(void *aContext) +{ + esp_openthread_platform_config_t config = { + .radio_config = ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG(), + .host_config = ESP_OPENTHREAD_DEFAULT_HOST_CONFIG(), + .port_config = ESP_OPENTHREAD_DEFAULT_PORT_CONFIG(), + }; + + esp_netif_inherent_config_t esp_netif_config = ESP_NETIF_INHERENT_DEFAULT_OPENTHREAD(); + esp_netif_config.if_desc = EXAMPLE_NETIF_DESC_THREAD; + esp_netif_config_t cfg = { + .base = &esp_netif_config, + .stack = &g_esp_netif_netstack_default_openthread, + }; + s_openthread_netif = esp_netif_new(&cfg); + assert(s_openthread_netif != NULL); + + // Initialize the OpenThread stack + ESP_ERROR_CHECK(esp_openthread_init(&config)); + ESP_ERROR_CHECK(esp_netif_attach(s_openthread_netif, esp_openthread_netif_glue_init(&config))); + esp_openthread_lock_acquire(portMAX_DELAY); + (void)otLoggingSetLevel(CONFIG_LOG_DEFAULT_LEVEL); + esp_openthread_cli_init(); + esp_openthread_cli_create_task(); + otOperationalDatasetTlvs dataset; + otError error = otDatasetGetActiveTlvs(esp_openthread_get_instance(), &dataset); + if (error != OT_ERROR_NONE) { + ESP_ERROR_CHECK(esp_openthread_auto_start(NULL)); + } else { + ESP_ERROR_CHECK(esp_openthread_auto_start(&dataset)); + } + esp_openthread_lock_release(); + + // Run the main loop + esp_openthread_launch_mainloop(); + + // Clean up + esp_openthread_netif_glue_deinit(); + esp_netif_destroy(s_openthread_netif); + esp_vfs_eventfd_unregister(); + vTaskDelete(NULL); +} + +/* tear down connection, release resources */ +void example_thread_shutdown(void) +{ + vTaskDelete(s_ot_task_handle); + esp_openthread_netif_glue_deinit(); + esp_netif_destroy(s_openthread_netif); + esp_vfs_eventfd_unregister(); + vSemaphoreDelete(s_semph_thread_set_dns_server); + vSemaphoreDelete(s_semph_thread_attached); +} + +esp_err_t example_thread_connect(void) +{ + s_semph_thread_attached = xSemaphoreCreateBinary(); + if (s_semph_thread_attached == NULL) { + return ESP_ERR_NO_MEM; + } + s_semph_thread_set_dns_server = xSemaphoreCreateBinary(); + if (s_semph_thread_set_dns_server == NULL) { + vSemaphoreDelete(s_semph_thread_attached); + return ESP_ERR_NO_MEM; + } + // 4 eventfds might be used for Thread + // * netif + // * ot task queue + // * radio driver + // * border router + esp_vfs_eventfd_config_t eventfd_config = { + .max_fds = 4, + }; + esp_vfs_eventfd_register(&eventfd_config); + ESP_ERROR_CHECK(esp_event_handler_register(OPENTHREAD_EVENT, ESP_EVENT_ANY_ID, thread_event_handler, NULL)); + if (xTaskCreate(ot_task_worker, "ot_br_main", CONFIG_EXAMPLE_THREAD_TASK_STACK_SIZE, NULL, 5, &s_ot_task_handle) != pdPASS) { + vSemaphoreDelete(s_semph_thread_attached); + vSemaphoreDelete(s_semph_thread_set_dns_server); + ESP_LOGE(TAG, "Failed to create openthread task"); + return ESP_FAIL; + } + xSemaphoreTake(s_semph_thread_attached, portMAX_DELAY); + // Wait 1s for the Thread device to set its DNS server with the NAT64 prefix. + if (xSemaphoreTake(s_semph_thread_set_dns_server, 1000 / portTICK_PERIOD_MS) != pdPASS) { + ESP_LOGW(TAG, "DNS server is not set for the Thread device, might be unable to access the Internet"); + } + return ESP_OK; +} diff --git a/components/protocol_examples_common/thread_connect.c:Zone.Identifier b/components/protocol_examples_common/thread_connect.c:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x +#include "protocol_examples_common.h" +#include "example_common_private.h" +#include "esp_log.h" + +#if CONFIG_EXAMPLE_CONNECT_WIFI + +static const char *TAG = "example_connect"; +static esp_netif_t *s_example_sta_netif = NULL; +static SemaphoreHandle_t s_semph_get_ip_addrs = NULL; +#if CONFIG_EXAMPLE_CONNECT_IPV6 +static SemaphoreHandle_t s_semph_get_ip6_addrs = NULL; +#endif + +static int s_retry_num = 0; + +static void example_handler_on_wifi_disconnect(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + s_retry_num++; + if (s_retry_num > CONFIG_EXAMPLE_WIFI_CONN_MAX_RETRY) { + ESP_LOGI(TAG, "WiFi Connect failed %d times, stop reconnect.", s_retry_num); + /* let example_wifi_sta_do_connect() return */ + if (s_semph_get_ip_addrs) { + xSemaphoreGive(s_semph_get_ip_addrs); + } +#if CONFIG_EXAMPLE_CONNECT_IPV6 + if (s_semph_get_ip6_addrs) { + xSemaphoreGive(s_semph_get_ip6_addrs); + } +#endif + example_wifi_sta_do_disconnect(); + return; + } + wifi_event_sta_disconnected_t *disconn = event_data; + if (disconn->reason == WIFI_REASON_ROAMING) { + ESP_LOGD(TAG, "station roaming, do nothing"); + return; + } + ESP_LOGI(TAG, "Wi-Fi disconnected %d, trying to reconnect...", disconn->reason); + esp_err_t err = esp_wifi_connect(); + if (err == ESP_ERR_WIFI_NOT_STARTED) { + return; + } + ESP_ERROR_CHECK(err); +} + +static void example_handler_on_wifi_connect(void *esp_netif, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ +#if CONFIG_EXAMPLE_CONNECT_IPV6 + esp_netif_create_ip6_linklocal(esp_netif); +#endif // CONFIG_EXAMPLE_CONNECT_IPV6 +} + +static void example_handler_on_sta_got_ip(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + s_retry_num = 0; + ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; + if (!example_is_our_netif(EXAMPLE_NETIF_DESC_STA, event->esp_netif)) { + return; + } + ESP_LOGI(TAG, "Got IPv4 event: Interface \"%s\" address: " IPSTR, esp_netif_get_desc(event->esp_netif), IP2STR(&event->ip_info.ip)); + if (s_semph_get_ip_addrs) { + xSemaphoreGive(s_semph_get_ip_addrs); + } else { + ESP_LOGI(TAG, "- IPv4 address: " IPSTR ",", IP2STR(&event->ip_info.ip)); + } +} + +#if CONFIG_EXAMPLE_CONNECT_IPV6 +static void example_handler_on_sta_got_ipv6(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + ip_event_got_ip6_t *event = (ip_event_got_ip6_t *)event_data; + if (!example_is_our_netif(EXAMPLE_NETIF_DESC_STA, event->esp_netif)) { + return; + } + esp_ip6_addr_type_t ipv6_type = esp_netif_ip6_get_addr_type(&event->ip6_info.ip); + ESP_LOGI(TAG, "Got IPv6 event: Interface \"%s\" address: " IPV6STR ", type: %s", esp_netif_get_desc(event->esp_netif), + IPV62STR(event->ip6_info.ip), example_ipv6_addr_types_to_str[ipv6_type]); + + if (ipv6_type == EXAMPLE_CONNECT_PREFERRED_IPV6_TYPE) { + if (s_semph_get_ip6_addrs) { + xSemaphoreGive(s_semph_get_ip6_addrs); + } else { + ESP_LOGI(TAG, "- IPv6 address: " IPV6STR ", type: %s", IPV62STR(event->ip6_info.ip), example_ipv6_addr_types_to_str[ipv6_type]); + } + } +} +#endif // CONFIG_EXAMPLE_CONNECT_IPV6 + + +void example_wifi_start(void) +{ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + esp_netif_inherent_config_t esp_netif_config = ESP_NETIF_INHERENT_DEFAULT_WIFI_STA(); + // Warning: the interface desc is used in tests to capture actual connection details (IP, gw, mask) + esp_netif_config.if_desc = EXAMPLE_NETIF_DESC_STA; + esp_netif_config.route_prio = 128; + s_example_sta_netif = esp_netif_create_wifi(WIFI_IF_STA, &esp_netif_config); + esp_wifi_set_default_wifi_sta_handlers(); + + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_start()); +} + + +void example_wifi_stop(void) +{ + esp_err_t err = esp_wifi_stop(); + if (err == ESP_ERR_WIFI_NOT_INIT) { + return; + } + ESP_ERROR_CHECK(err); + ESP_ERROR_CHECK(esp_wifi_deinit()); + ESP_ERROR_CHECK(esp_wifi_clear_default_wifi_driver_and_handlers(s_example_sta_netif)); + esp_netif_destroy(s_example_sta_netif); + s_example_sta_netif = NULL; +} + + +esp_err_t example_wifi_sta_do_connect(wifi_config_t wifi_config, bool wait) +{ + if (wait) { + s_semph_get_ip_addrs = xSemaphoreCreateBinary(); + if (s_semph_get_ip_addrs == NULL) { + return ESP_ERR_NO_MEM; + } +#if CONFIG_EXAMPLE_CONNECT_IPV6 + s_semph_get_ip6_addrs = xSemaphoreCreateBinary(); + if (s_semph_get_ip6_addrs == NULL) { + vSemaphoreDelete(s_semph_get_ip_addrs); + return ESP_ERR_NO_MEM; + } +#endif + } + s_retry_num = 0; + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &example_handler_on_wifi_disconnect, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &example_handler_on_sta_got_ip, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &example_handler_on_wifi_connect, s_example_sta_netif)); +#if CONFIG_EXAMPLE_CONNECT_IPV6 + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &example_handler_on_sta_got_ipv6, NULL)); +#endif + + ESP_LOGI(TAG, "Connecting to %s...", wifi_config.sta.ssid); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); + esp_err_t ret = esp_wifi_connect(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "WiFi connect failed! ret:%x", ret); + return ret; + } + if (wait) { + ESP_LOGI(TAG, "Waiting for IP(s)"); +#if CONFIG_EXAMPLE_CONNECT_IPV4 + xSemaphoreTake(s_semph_get_ip_addrs, portMAX_DELAY); + vSemaphoreDelete(s_semph_get_ip_addrs); + s_semph_get_ip_addrs = NULL; +#endif +#if CONFIG_EXAMPLE_CONNECT_IPV6 + xSemaphoreTake(s_semph_get_ip6_addrs, portMAX_DELAY); + vSemaphoreDelete(s_semph_get_ip6_addrs); + s_semph_get_ip6_addrs = NULL; +#endif + if (s_retry_num > CONFIG_EXAMPLE_WIFI_CONN_MAX_RETRY) { + return ESP_FAIL; + } + } + return ESP_OK; +} + +esp_err_t example_wifi_sta_do_disconnect(void) +{ + ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &example_handler_on_wifi_disconnect)); + ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &example_handler_on_sta_got_ip)); + ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &example_handler_on_wifi_connect)); +#if CONFIG_EXAMPLE_CONNECT_IPV6 + ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_GOT_IP6, &example_handler_on_sta_got_ipv6)); +#endif + return esp_wifi_disconnect(); +} + +void example_wifi_shutdown(void) +{ + example_wifi_sta_do_disconnect(); + example_wifi_stop(); +} + +esp_err_t example_wifi_connect(void) +{ + ESP_LOGI(TAG, "Start example_connect."); + example_wifi_start(); + wifi_config_t wifi_config = { + .sta = { +#if !CONFIG_EXAMPLE_WIFI_SSID_PWD_FROM_STDIN + .ssid = CONFIG_EXAMPLE_WIFI_SSID, + .password = CONFIG_EXAMPLE_WIFI_PASSWORD, +#endif + .scan_method = EXAMPLE_WIFI_SCAN_METHOD, + .sort_method = EXAMPLE_WIFI_CONNECT_AP_SORT_METHOD, + .threshold.rssi = CONFIG_EXAMPLE_WIFI_SCAN_RSSI_THRESHOLD, + .threshold.authmode = EXAMPLE_WIFI_SCAN_AUTH_MODE_THRESHOLD, + }, + }; +#if CONFIG_EXAMPLE_WIFI_SSID_PWD_FROM_STDIN + example_configure_stdin_stdout(); + char buf[sizeof(wifi_config.sta.ssid)+sizeof(wifi_config.sta.password)+2] = {0}; + ESP_LOGI(TAG, "Please input ssid password:"); + fgets(buf, sizeof(buf), stdin); + int len = strlen(buf); + buf[len-1] = '\0'; /* removes '\n' */ + memset(wifi_config.sta.ssid, 0, sizeof(wifi_config.sta.ssid)); + + char *rest = NULL; + char *temp = strtok_r(buf, " ", &rest); + strncpy((char*)wifi_config.sta.ssid, temp, sizeof(wifi_config.sta.ssid)); + memset(wifi_config.sta.password, 0, sizeof(wifi_config.sta.password)); + temp = strtok_r(NULL, " ", &rest); + if (temp) { + strncpy((char*)wifi_config.sta.password, temp, sizeof(wifi_config.sta.password)); + } else { + wifi_config.sta.threshold.authmode = WIFI_AUTH_OPEN; + } +#endif + return example_wifi_sta_do_connect(wifi_config, true); +} + + +#endif /* CONFIG_EXAMPLE_CONNECT_WIFI */ diff --git a/components/protocol_examples_common/wifi_connect.c:Zone.Identifier b/components/protocol_examples_common/wifi_connect.c:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2x +#include "serial_mcu.h" +#include "esp_log.h" +#include "driver/uart.h" +#include "esp_err.h" +#include "string.h" +#include "driver/gpio.h" +// 这里是 serial_mcu.c 的实现代码 + +static const char *TAG = "serial_mcu"; + + +#define TXD_PIN (GPIO_NUM_21) // TXD 引脚 +#define RXD_PIN (GPIO_NUM_20) // RXD 引脚 +#define UART_PORT_NUM UART_NUM_1 // 使用 UART1 + +static const int RX_BUF_SIZE = 1024; // 接收缓冲区大小 + +// 初始化串口 +void serial_mcu_init(void) +{ + // TODO: 初始化串口 + ESP_LOGD(TAG, "Serial MCU initialized"); + + const uart_config_t uart_config = { + .baud_rate = 230400, // 波特率 + .data_bits = UART_DATA_8_BITS, // 数据位 + .parity = UART_PARITY_DISABLE, // 校验位 + .stop_bits = UART_STOP_BITS_1, // 停止位 + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, // 流控 + .source_clk = UART_SCLK_DEFAULT, // 时钟源 + }; + + // 配置串口 + uart_driver_install(UART_PORT_NUM, RX_BUF_SIZE * 2, 0, 0, NULL, 0); + uart_param_config(UART_PORT_NUM, &uart_config); + uart_set_pin(UART_PORT_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); // 设置引脚 + // 日志 + ESP_LOGI(TAG, "UART driver installed");} + +int sendControlFrame(uint8_t cmd, uint8_t data) +{ + uint8_t buf[3] = {0x55, cmd, data}; + int txBytes = uart_write_bytes(UART_NUM_1, (const char*)buf, 3); + ESP_LOGI("TX", "Sent %d bytes CMD=0x%02X DATA=%d", txBytes, cmd, data); + return txBytes; +} diff --git a/dependencies.lock b/dependencies.lock new file mode 100644 index 0000000..b642695 --- /dev/null +++ b/dependencies.lock @@ -0,0 +1,108 @@ +dependencies: + espressif/bh1750: + component_hash: e898130f6b2fc4bc0d6022a2e431752bae808b9c94d4cc91596e36ecaf4cb7c6 + dependencies: + - name: idf + require: private + version: '>=5.3' + source: + registry_url: https://components.espressif.com/ + type: service + version: 2.0.0 + espressif/cjson: + component_hash: 9372811fb197926f522c467627cf4a8e72b681e0366e17879631da801103aef3 + dependencies: + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.7.19 + espressif/esp_lvgl_port: + component_hash: f872401524cb645ee6ff1c9242d44fb4ddcfd4d37d7be8b9ed3f4e85a404efcd + dependencies: + - name: idf + require: private + version: '>=5.1' + - name: lvgl/lvgl + registry_url: https://components.espressif.com + require: public + version: '>=8,<10' + source: + registry_url: https://components.espressif.com/ + type: service + version: 2.7.0 + espressif/mqtt: + component_hash: ffdad5659706b4dc14bc63f8eb73ef765efa015bf7e9adf71c813d52a2dc9342 + dependencies: + - name: idf + require: private + version: '>=5.3' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.0.0 + espressif/servo: + component_hash: 309c787e48224255fad458cfd9ab86ea53f0fdad1c5e4f6f0c50309990b17108 + dependencies: + - name: idf + require: private + version: '>=4.4' + source: + registry_url: https://components.espressif.com/ + type: service + version: 0.1.0 + idf: + source: + type: idf + version: 5.5.2 + k0i05/esp_ahtxx: + component_hash: 34ecd4cc05b54a8ee64a813f80cf2b8efea6f22ecdbf7244640fc29627416fed + dependencies: + - name: idf + require: private + version: '>5.3.0' + - name: k0i05/esp_type_utils + registry_url: https://components.espressif.com + require: private + version: '>=1.0.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.2.7 + k0i05/esp_type_utils: + component_hash: 95d8ec40268e045f7e264d8035f451e53844b4a2f6d5f112ece6645c5effd639 + dependencies: + - name: idf + require: private + version: '>5.3.0' + source: + registry_url: https://components.espressif.com + type: service + version: 1.2.7 + lvgl/lvgl: + component_hash: 17e68bfd21f0edf4c3ee838e2273da840bf3930e5dbc3bfa6c1190c3aed41f9f + dependencies: [] + source: + registry_url: https://components.espressif.com + type: service + version: 9.4.0 + protocol_examples_common: + dependencies: [] + source: + path: /home/beihong/esp_projects/iot-home/components/protocol_examples_common + type: local + version: '*' +direct_dependencies: +- espressif/bh1750 +- espressif/cjson +- espressif/esp_lvgl_port +- espressif/mqtt +- espressif/servo +- idf +- k0i05/esp_ahtxx +- protocol_examples_common +manifest_hash: 1bbfddd3f393c249aa2425f33f0a08f08c36fdbae7efc3287e867104a15c7d27 +target: esp32c3 +version: 2.0.0 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..6220cc0 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register(SRCS "main.c" + INCLUDE_DIRS "." + PRIV_REQUIRES esp_wifi cjson nvs_flash lvgl_st7735s_use esp_driver_i2c esp_type_utils esp_timer espressif__servo esp_event esp_netif serial_mcu mqtt + WHOLE_ARCHIVE + ) + diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..91d1b27 --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,31 @@ +## IDF Component Manager Manifest File +dependencies: + ## Required IDF version + idf: + version: '>=4.1.0' + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true + + protocol_examples_common: + path: ../components/protocol_examples_common + + + espressif/esp_lvgl_port: ^2.7.0 + + k0i05/esp_ahtxx: ^1.2.7 + espressif/bh1750: ^2.0.0 + espressif/servo: ^0.1.0 + + espressif/mqtt: ^1.0.0 + espressif/cjson: ^1.7.19 + + diff --git a/main/main.c b/main/main.c new file mode 100644 index 0000000..45c12d1 --- /dev/null +++ b/main/main.c @@ -0,0 +1,3127 @@ +/** + * @file main.c + * @brief IoT智能家居控制系统主程序 + * @author beihong.wang + * @version 1.0 + * @date 2026-01-19 + */ + +/* ========================= 1. 头文件包含 ========================= */ +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "sdkconfig.h" +#include "driver/gpio.h" +#include "driver/adc.h" +#include "driver/i2c_master.h" +#include "driver/uart.h" +#include "esp_sntp.h" +#include "esp_netif_sntp.h" +#include "sys/time.h" +#include "esp_timer.h" +#include "esp_log.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_wifi.h" +#include "mqtt_client.h" +#include "cJSON.h" +#include "nvs_flash.h" + +// 组件头文件 +#include "lvgl_st7735s_use.h" +#include "ahtxx.h" +#include "bh1750.h" +#include "ui_display.h" +#include "iot_servo.h" +#include "serial_mcu.h" +#include "protocol_examples_common.h" + +/* ========================= 2. 宏定义 ========================= */ +// 日志标签 +#define TAG "main" +#define AHT30_TAG "i2c0_ahtxx_task" +#define BH1750_TAG "i2c0_bh1750_task" +#define MQ135_TAG "mq135_task" +#define LVGL_TAG "lvgl_task" +#define SERVO_TAG "servo_task" +#define SNTP_TAG "sntp_task" +#define Clock "AlarmClock" +#define TIME_PERIOD_TAG "TimePeriod" + +// NVS命名空间 +#define NVS_NAMESPACE "alarms" +#define TIME_PERIOD_NAMESPACE "time_period" +#define COOLING_MODE_NAMESPACE "cooling_mode" + +// 硬件配置 +#define I2C_MASTER_SCL_IO 5 +#define I2C_MASTER_SDA_IO 4 +#define I2C_MASTER_NUM I2C_NUM_0 +#define I2C_MASTER_FREQ_HZ 100000 +#define SERVO_GPIO (10) + +// 闹钟配置 +#define ALARM_MAX_NUM 3 +#define BUZZER_DELAY_MS 5000 +#define ALARM_WAKEUP_PRE_MINUTES 3 // 起床前提前开窗帘时间(分钟) + +// MQTT配置 +#define MQTT_BROKER_URL "mqtt://beihong.wang:1883" +#define MQTT_USERNAME "esp_mqtt_client" +#define MQTT_CLIENT_ID "esp_mqtt_client" +#define MQTT_PASSWORD "664hd78gas97" +#define MQTT_PUBLISH_TOPIC_QOS0 "topic/sensor/esp32_iothome_001" +#define MQTT_NOTIFY_TOPIC "topic/control/esp32_iothome_001" +#define MQTT_UNSUBSCRIBE_TOPIC "topic/control/esp32_iothome_001" + +/* ========================= 3. 结构体定义 ========================= */ + +// 时间段类型 +typedef enum +{ + TIME_PERIOD_DAY = 0, + TIME_PERIOD_NIGHT = 1, +} time_period_type_t; + +// 光强状态枚举 +typedef enum +{ + LIGHT_STATE_UNKNOWN = 0, + LIGHT_STATE_BRIGHT, + LIGHT_STATE_DIM, + LIGHT_STATE_MODERATE, +} light_state_t; + +// 时间段配置结构体 +typedef struct +{ + uint8_t start_hour; + uint8_t start_minute; + uint8_t end_hour; + uint8_t end_minute; + bool enabled; +} time_period_config_t; + +// 闹钟时间结构体 +typedef struct +{ + uint8_t hour; + uint8_t minute; + uint8_t second; + bool enable; + bool triggered; + bool curtain_opened; // 标记窗帘是否已提前打开 + TaskHandle_t task_hdl; +} alarm_time_t; + +// 传感器数据结构体 +typedef struct +{ + float temperature; + float humidity; + float lux; + float air_quality; + bool ahtxx_valid; + bool bh1750_valid; + bool mq135_valid; +} sensor_data_t; + +// 设备状态结构体 +typedef struct +{ + bool online; + char current_mode[20]; + bool home_status; + bool standby_mode; + uint8_t error_code; +} device_state_t; + +// 遥测数据结构体 +typedef struct +{ + float temperature; + float humidity; + float light_intensity; + uint16_t air_quality; + char curtain_state[10]; + char led_state[10]; + uint8_t led_power; + char fan_state[10]; + char buzzer_state[10]; +} telemetry_data_t; + +// 设备消息结构体 +typedef struct +{ + char device_id[32]; + char device_type[32]; + uint64_t timestamp; + device_state_t state; + telemetry_data_t telemetry; +} device_message_t; + +/* ========================= 4. 全局变量 ========================= */ + +// 控制标志 +bool led_backlight_on = true; +bool servo_control_flag = false; +bool fan_control_flag = false; +bool buzzer_control_flag = false; +bool light_source_control_flag = false; +uint8_t led_brightness_value = 0; + +// 降温模式配置 +#define COOLING_MODE_TAG "cooling_mode" +float g_temperature_threshold = 28.0f; // 降温温度阈值(摄氏度) +bool g_cooling_mode_enabled = true; // 降温模式是否启用 +bool g_high_temp_alerted = false; // 高温提醒是否已发送(避免重复提醒) + +// 自动通风控制模式配置 +#define VENTILATION_MODE_TAG "ventilation_mode" +#define AIR_QUALITY_THRESHOLD 50.0f // 空气质量阈值 +bool g_ventilation_mode_enabled = true; // 通风模式是否启用 +bool g_air_quality_alerted = false; // 空气质量提醒是否已发送(避免重复提醒) + +// 时间段配置 +time_period_config_t g_day_period = { + .start_hour = 6, .start_minute = 0, .end_hour = 18, .end_minute = 0, .enabled = true}; +time_period_config_t g_night_period = { + .start_hour = 18, .start_minute = 0, .end_hour = 6, .end_minute = 0, .enabled = true}; +time_period_type_t g_current_period = TIME_PERIOD_DAY; +bool g_period_initialized = false; + +// 闹钟数据 +alarm_time_t g_alarms[ALARM_MAX_NUM] = { + {.hour = 10, .minute = 0, .second = 0, .enable = false, .triggered = false, .curtain_opened = false, .task_hdl = NULL}, + {.hour = 12, .minute = 0, .second = 0, .enable = false, .triggered = false, .curtain_opened = false, .task_hdl = NULL}, + {.hour = 18, .minute = 0, .second = 0, .enable = false, .triggered = false, .curtain_opened = false, .task_hdl = NULL}}; +esp_timer_handle_t g_alarm_check_timer = NULL; + +// 传感器数据 +sensor_data_t g_sensor_data = {0}; + +// MQTT数据 +static esp_mqtt_client_handle_t g_mqtt_client = NULL; +static device_message_t g_device_message = {0}; + +// 互斥锁 +static SemaphoreHandle_t xTimePeriodMutex = NULL; +SemaphoreHandle_t xSensorDataMutex = NULL; +static SemaphoreHandle_t xMqttMessageMutex = NULL; +static SemaphoreHandle_t xControlFlagMutex = NULL; // 控制标志互斥锁 + +// I2C句柄 +i2c_master_bus_handle_t bus_handle; +i2c_master_dev_handle_t dev_handle; + +// 舵机校准值 +static uint16_t calibration_value_0 = 30; +static uint16_t calibration_value_180 = 195; + +/* ========================= 5. 函数前向声明 ========================= */ + +// 工具函数 +static void get_local_time(struct tm *tm_now); +void servo_set_angle(uint16_t angle); +void get_sensor_data(sensor_data_t *data); +void print_sensor_data(void); + +// NVS函数 +void initialize_nvs(void); + +// 时间段相关函数 +static void time_period_save_to_nvs(time_period_type_t period_type); +static void time_period_load_from_nvs(time_period_type_t period_type); +static void time_period_set(time_period_type_t period_type, uint8_t start_hour, uint8_t start_minute, + uint8_t end_hour, uint8_t end_minute); +static bool is_time_in_period(time_period_config_t *config, uint8_t current_hour, uint8_t current_minute); +static void on_day_period_event(void); +static void on_night_period_event(void); +static void time_period_check_task(void *pvParameters); + +// MQTT辅助函数 +static void update_telemetry_and_report(void); + +// 闹钟相关函数 +static void alarm_trigger_action(uint8_t alarm_idx); +static void alarm_stop_action(void *param); +static void alarm_check_timer_cb(void *arg); +static esp_err_t alarm_timer_init(void); +static void alarm_save_to_nvs(uint8_t alarm_idx); +static void alarm_load_from_nvs(uint8_t alarm_idx); +void alarm_set_time(uint8_t alarm_idx, uint8_t hour, uint8_t minute, uint8_t second); +void alarm_set_enable(uint8_t alarm_idx, bool enable); +void alarm_disable_all(void); + +// SNTP相关函数 +static void set_timezone(void); +static time_t get_current_time(void); +static void print_current_time(void); +static void initialize_sntp(void); + +// MQTT相关函数 +void mqtt_publish_task(void *pvParameters); +void mqtt_publish_feedback(void); +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); +static void mqtt_app_start(void); + +// 硬件初始化函数 +static void servo_init(void); +static esp_err_t i2c_master_init(void); +void init_gpio_output(void); + +// 传感器任务 +void i2c0_ahtxx_task(void *pvParameters); +void i2c0_bh1750_task(void *pvParameters); +void mq135_task(void *pvParameters); + +// 其他任务 +static void rx_task(void *arg); +void peripheral_control_task(void *pvParameters); +static void alarm_clock_main_task(void *arg); +static void cooling_mode_task(void *pvParameters); +static void ventilation_mode_task(void *pvParameters); + +/** + * @brief 将时间段配置保存到 NVS + * @param period_type 时间段类型 (DAY/NIGHT) + */ +static void time_period_save_to_nvs(time_period_type_t period_type) +{ + time_period_config_t *config = (period_type == TIME_PERIOD_DAY) ? &g_day_period : &g_night_period; + const char *period_name = (period_type == TIME_PERIOD_DAY) ? "day" : "night"; + + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open(TIME_PERIOD_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) + { + ESP_LOGE(TIME_PERIOD_TAG, "Failed to open NVS: %s", esp_err_to_name(err)); + return; + } + + char key_config[32], key_enable[32]; + snprintf(key_config, sizeof(key_config), "%s_period_cfg", period_name); + snprintf(key_enable, sizeof(key_enable), "%s_period_en", period_name); + + // 把时间编码为单个32位整数:(start_hour << 24) | (start_minute << 16) | (end_hour << 8) | end_minute + uint32_t time_data = ((uint32_t)config->start_hour << 24) | + ((uint32_t)config->start_minute << 16) | + ((uint32_t)config->end_hour << 8) | + config->end_minute; + + nvs_set_u32(nvs_handle, key_config, time_data); + nvs_set_u8(nvs_handle, key_enable, config->enabled ? 1 : 0); + nvs_commit(nvs_handle); + nvs_close(nvs_handle); + + ESP_LOGI(TIME_PERIOD_TAG, "%s period saved to NVS: %02d:%02d - %02d:%02d (enabled=%d)", + period_name, config->start_hour, config->start_minute, + config->end_hour, config->end_minute, config->enabled); +} + +/** + * @brief 从 NVS 读取时间段配置 + * @param period_type 时间段类型 (DAY/NIGHT) + */ +static void time_period_load_from_nvs(time_period_type_t period_type) +{ + time_period_config_t *config = (period_type == TIME_PERIOD_DAY) ? &g_day_period : &g_night_period; + const char *period_name = (period_type == TIME_PERIOD_DAY) ? "day" : "night"; + + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open(TIME_PERIOD_NAMESPACE, NVS_READONLY, &nvs_handle); + if (err != ESP_OK) + { + ESP_LOGI(TIME_PERIOD_TAG, "NVS not found for %s period, using default", period_name); + return; + } + + char key_config[32], key_enable[32]; + snprintf(key_config, sizeof(key_config), "%s_period_cfg", period_name); + snprintf(key_enable, sizeof(key_enable), "%s_period_en", period_name); + + uint32_t time_data = 0; + uint8_t enable_data = 0; + + if (nvs_get_u32(nvs_handle, key_config, &time_data) == ESP_OK) + { + config->start_hour = (time_data >> 24) & 0xFF; + config->start_minute = (time_data >> 16) & 0xFF; + config->end_hour = (time_data >> 8) & 0xFF; + config->end_minute = time_data & 0xFF; + } + + if (nvs_get_u8(nvs_handle, key_enable, &enable_data) == ESP_OK) + { + config->enabled = (enable_data != 0); + } + + nvs_close(nvs_handle); + + ESP_LOGI(TIME_PERIOD_TAG, "%s period loaded from NVS: %02d:%02d - %02d:%02d (enabled=%d)", + period_name, config->start_hour, config->start_minute, + config->end_hour, config->end_minute, config->enabled); +} + +/** + * @brief 设置时间段 + * @param period_type 时间段类型 (DAY/NIGHT) + * @param start_hour 开始小时 (0-23) + * @param start_minute 开始分钟 (0-59) + * @param end_hour 结束小时 (0-23) + * @param end_minute 结束分钟 (0-59) + */ +static void time_period_set(time_period_type_t period_type, uint8_t start_hour, uint8_t start_minute, + uint8_t end_hour, uint8_t end_minute) +{ + if (start_hour > 23 || start_minute > 59 || end_hour > 23 || end_minute > 59) + { + ESP_LOGE(TIME_PERIOD_TAG, "Invalid time parameters"); + return; + } + + if (xTimePeriodMutex != NULL && xSemaphoreTake(xTimePeriodMutex, portMAX_DELAY) == pdTRUE) + { + time_period_config_t *config = (period_type == TIME_PERIOD_DAY) ? &g_day_period : &g_night_period; + const char *period_name = (period_type == TIME_PERIOD_DAY) ? "day" : "night"; + + config->start_hour = start_hour; + config->start_minute = start_minute; + config->end_hour = end_hour; + config->end_minute = end_minute; + config->enabled = true; + + xSemaphoreGive(xTimePeriodMutex); + + ESP_LOGI(TIME_PERIOD_TAG, "%s period set: %02d:%02d - %02d:%02d", + period_name, start_hour, start_minute, end_hour, end_minute); + + time_period_save_to_nvs(period_type); + } +} + +/** + * @brief 检查当前时间是否在指定时间段内(支持跨天) + * @param config 时间段配置 + * @param current_hour 当前小时 + * @param current_minute 当前分钟 + * @return true 在时间段内,false 不在时间段内 + */ +static bool is_time_in_period(time_period_config_t *config, uint8_t current_hour, uint8_t current_minute) +{ + if (!config->enabled) + { + return false; + } + + // 将时间转换为分钟数便于比较 + int start_time = config->start_hour * 60 + config->start_minute; + int end_time = config->end_hour * 60 + config->end_minute; + int current_time = current_hour * 60 + current_minute; + + if (start_time <= end_time) + { + // 不跨天的情况,例如 06:00 - 18:00 + return (current_time >= start_time && current_time < end_time); + } + else + { + // 跨天的情况,例如 18:00 - 06:00 + return (current_time >= start_time || current_time < end_time); + } +} + +/** + * @brief 保存温度阈值到 NVS + */ +static void cooling_mode_save_to_nvs(void) +{ + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open(COOLING_MODE_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) + { + ESP_LOGE(COOLING_MODE_TAG, "Failed to open NVS: %s", esp_err_to_name(err)); + return; + } + + nvs_set_u8(nvs_handle, "temp_threshold", (uint8_t)g_temperature_threshold); + nvs_set_u8(nvs_handle, "cooling_enabled", g_cooling_mode_enabled ? 1 : 0); + nvs_commit(nvs_handle); + nvs_close(nvs_handle); + + ESP_LOGI(COOLING_MODE_TAG, "Cooling mode saved to NVS: threshold=%.1f°C, enabled=%d", + g_temperature_threshold, g_cooling_mode_enabled); +} + +/** + * @brief 从 NVS 读取温度阈值配置 + */ +static void cooling_mode_load_from_nvs(void) +{ + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open(COOLING_MODE_NAMESPACE, NVS_READONLY, &nvs_handle); + if (err != ESP_OK) + { + ESP_LOGI(COOLING_MODE_TAG, "NVS not found for cooling mode, using defaults"); + return; + } + + uint8_t threshold_data = 0; + uint8_t enabled_data = 0; + + if (nvs_get_u8(nvs_handle, "temp_threshold", &threshold_data) == ESP_OK) + { + g_temperature_threshold = (float)threshold_data; + } + + if (nvs_get_u8(nvs_handle, "cooling_enabled", &enabled_data) == ESP_OK) + { + g_cooling_mode_enabled = (enabled_data != 0); + } + + nvs_close(nvs_handle); + + ESP_LOGI(COOLING_MODE_TAG, "Cooling mode loaded from NVS: threshold=%.1f°C, enabled=%d", + g_temperature_threshold, g_cooling_mode_enabled); +} + +/** + * @brief 时间段检查任务 + * 定期检查当前时间,判断是否进入/离开某个时间段 + * 在时间段内根据光强变化执行相应动作 + */ +static void time_period_check_task(void *pvParameters) +{ + ESP_LOGI(TIME_PERIOD_TAG, "Time period check task started"); + + // 加载NVS中的时间段配置 + time_period_load_from_nvs(TIME_PERIOD_DAY); + time_period_load_from_nvs(TIME_PERIOD_NIGHT); + + g_period_initialized = true; + + time_period_type_t last_period = TIME_PERIOD_DAY; + light_state_t last_light_state = LIGHT_STATE_UNKNOWN; + bool first_check = true; + + while (1) + { + struct tm tm_now; + get_local_time(&tm_now); + + if (xTimePeriodMutex != NULL && xSemaphoreTake(xTimePeriodMutex, portMAX_DELAY) == pdTRUE) + { + time_period_type_t current_period; + + // 判断当前处于哪个时间段 + if (is_time_in_period(&g_day_period, tm_now.tm_hour, tm_now.tm_min)) + { + current_period = TIME_PERIOD_DAY; + } + else if (is_time_in_period(&g_night_period, tm_now.tm_hour, tm_now.tm_min)) + { + current_period = TIME_PERIOD_NIGHT; + } + else + { + // 不在任何配置的时间段内,保持上一次状态 + current_period = last_period; + } + + g_current_period = current_period; + + // 时间段发生变化或首次检查时触发事件 + if (first_check || current_period != last_period) + { + ESP_LOGI(TIME_PERIOD_TAG, "Period changed: %s -> %s (time: %02d:%02d)", + (last_period == TIME_PERIOD_DAY) ? "day" : "night", + (current_period == TIME_PERIOD_DAY) ? "day" : "night", + tm_now.tm_hour, tm_now.tm_min); + + if (current_period == TIME_PERIOD_DAY) + { + on_day_period_event(); + } + else + { + on_night_period_event(); + } + + last_period = current_period; + first_check = false; + + // 重置光强状态,下次循环会重新检测 + last_light_state = LIGHT_STATE_UNKNOWN; + } + + xSemaphoreGive(xTimePeriodMutex); + } + + // 在白天模式期间,持续检测光强变化 + if (g_current_period == TIME_PERIOD_DAY) + { + float current_lux = 0; + bool lux_valid = false; + + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + current_lux = g_sensor_data.lux; + lux_valid = g_sensor_data.bh1750_valid; + xSemaphoreGive(xSensorDataMutex); + } + + if (lux_valid) + { + // 判断当前光强状态 + light_state_t current_light_state; + if (current_lux > 500.0f) + { + current_light_state = LIGHT_STATE_BRIGHT; + } + else if (current_lux < 200.0f) + { + current_light_state = LIGHT_STATE_DIM; + } + else + { + current_light_state = LIGHT_STATE_MODERATE; + } + + // 检查当前控制状态是否与光强推荐策略一致 + bool need_action = false; + if (current_light_state != last_light_state) + { + // 光强状态发生变化 + ESP_LOGI(TIME_PERIOD_TAG, "Light state changed: %d -> %d (lux=%.2f)", + last_light_state, current_light_state, current_lux); + need_action = true; + } + else + { + // 光强状态没变,检查手动控制是否与推荐策略冲突 + switch (current_light_state) + { + case LIGHT_STATE_BRIGHT: + // 推荐:关灯关窗帘。如果灯是开着的,需要执行动作 + if (light_source_control_flag == true) + { + ESP_LOGI(TIME_PERIOD_TAG, "Manual control detected: should be OFF but light is ON (lux=%.2f)", current_lux); + need_action = true; + } + break; + case LIGHT_STATE_DIM: + // 推荐:开灯开窗帘。如果灯是关着的,需要执行动作 + if (light_source_control_flag == false) + { + ESP_LOGI(TIME_PERIOD_TAG, "Manual control detected: should be ON but light is OFF (lux=%.2f)", current_lux); + need_action = true; + } + break; + case LIGHT_STATE_MODERATE: + // 推荐:开窗帘。如果窗帘是关着的,需要执行动作 + if (servo_control_flag == false) + { + ESP_LOGI(TIME_PERIOD_TAG, "Manual control detected: curtain should be OPEN but is CLOSED (lux=%.2f)", current_lux); + need_action = true; + } + break; + default: + break; + } + } + + // 执行动作 + if (need_action) + { + // 根据光强状态执行相应动作 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + switch (current_light_state) + { + case LIGHT_STATE_BRIGHT: + // 光照充足:关闭窗帘,关闭灯光 + servo_control_flag = false; + light_source_control_flag = false; + led_brightness_value = 0; + ESP_LOGI(TIME_PERIOD_TAG, "Action: Closing curtain and turning off light"); + break; + + case LIGHT_STATE_DIM: + // 光照不足:打开窗帘,打开灯光 + servo_control_flag = true; + light_source_control_flag = true; + led_brightness_value = 80; + ESP_LOGI(TIME_PERIOD_TAG, "Action: Opening curtain and turning on light"); + break; + + case LIGHT_STATE_MODERATE: + // 光照适中:打开窗帘(利用自然光) + servo_control_flag = true; + ESP_LOGI(TIME_PERIOD_TAG, "Action: Opening curtain for natural light"); + break; + + default: + break; + } + xSemaphoreGive(xControlFlagMutex); + } + + // 更新遥测数据并上报 + update_telemetry_and_report(); + + last_light_state = current_light_state; + } + } + } + + // 10秒检查一次(加快检测频率,及时响应光强变化) + vTaskDelay(pdMS_TO_TICKS(10000)); + } +} + +/** + * @brief 获取当前系统本地时间(已做时区补偿) + * @param tm_now 输出参数:当前时间的tm结构体 + */ +static void get_local_time(struct tm *tm_now) +{ + time_t now; + time(&now); + localtime_r(&now, tm_now); // 转为本地时间(关键,替代gmtime_r) +} + +/** + * @brief 【自定义】闹钟触发动作(适配多闹钟,传索引区分) + * @param alarm_idx 触发的闹钟索引(0~ALARM_MAX_NUM-1) + */ +static void alarm_trigger_action(uint8_t alarm_idx) +{ + // 索引合法性检查,防止越界 + if (alarm_idx >= ALARM_MAX_NUM) + { + ESP_LOGE(Clock, "Alarm trigger idx %d is invalid!", alarm_idx); + return; + } + // 打印触发的闹钟索引和时间,方便区分 + ESP_LOGI(Clock, "ALARM[%d] TRIGGERED! Time: %02d:%02d:%02d", + alarm_idx, + g_alarms[alarm_idx].hour, + g_alarms[alarm_idx].minute, + g_alarms[alarm_idx].second); + // 原有蜂鸣器控制逻辑,保留 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + buzzer_control_flag = true; + xSemaphoreGive(xControlFlagMutex); + } +} + +/** + * @brief 闹钟停止动作(适配多闹钟,传索引区分) + * @param param 传入闹钟索引(void*类型,兼容FreeRTOS任务传参) + */ +static void alarm_stop_action(void *param) +{ + // 解析传入的闹钟索引 + uint8_t alarm_idx = (uint8_t)(uint32_t)param; + // 索引合法性检查 + if (alarm_idx >= ALARM_MAX_NUM) + { + ESP_LOGE(Clock, "Alarm stop idx %d is invalid!", alarm_idx); + vTaskDelete(NULL); + return; + } + + // 原有延时逻辑,保留 + vTaskDelay(pdMS_TO_TICKS(BUZZER_DELAY_MS)); + + // 重置当前闹钟的触发标志、窗帘打开标志和蜂鸣器 + g_alarms[alarm_idx].triggered = false; + g_alarms[alarm_idx].curtain_opened = false; // 重置窗帘打开标志,为下次闹钟做准备 + + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + buzzer_control_flag = false; + xSemaphoreGive(xControlFlagMutex); + } + + ESP_LOGI(Clock, "ALARM[%d] STOPPED", alarm_idx); + + // 重置当前闹钟的任务句柄,删除自身任务 + g_alarms[alarm_idx].task_hdl = NULL; + vTaskDelete(NULL); +} + +/** + * @brief 闹钟时间检查回调函数(核心:遍历所有闹钟,逐个检查) + * @param arg 入参(未使用) + */ +static void alarm_check_timer_cb(void *arg) +{ + struct tm tm_now; + // 获取当前本地时间,只获取一次,避免多次调用系统函数 + get_local_time(&tm_now); + ESP_LOGD(Clock, "Current local time: %02d:%02d:%02d", + tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec); + + // 核心:遍历所有闹钟,逐个检查是否触发 + for (uint8_t i = 0; i < ALARM_MAX_NUM; i++) + { + // 跳过未使能的闹钟,减少无效判断 + if (!g_alarms[i].enable) + { + continue; + } + + // 计算当前时间到闹钟时间的分钟差 + int current_total_minutes = tm_now.tm_hour * 60 + tm_now.tm_min; + int alarm_total_minutes = g_alarms[i].hour * 60 + g_alarms[i].minute; + int minute_diff = alarm_total_minutes - current_total_minutes; + + // 只有闹钟0(闹钟1)支持提前3分钟自动开窗帘功能 + if (i == 0 && + minute_diff == ALARM_WAKEUP_PRE_MINUTES && + tm_now.tm_sec == 0 && + !g_alarms[i].curtain_opened) + { + // 打开窗帘(温和唤醒) + servo_control_flag = true; + g_alarms[i].curtain_opened = true; + ESP_LOGI(Clock, "ALARM[0] Wakeup mode: Opening curtain 3 minutes early (%02d:%02d)", + tm_now.tm_hour, tm_now.tm_min); + // 更新遥测并上报 + update_telemetry_and_report(); + } + + // 时间匹配 + 未触发 + 无正在运行的停止任务 → 触发闹钟 + if (tm_now.tm_hour == g_alarms[i].hour && + tm_now.tm_min == g_alarms[i].minute && + tm_now.tm_sec == g_alarms[i].second && + !g_alarms[i].triggered && + g_alarms[i].task_hdl == NULL) + { + g_alarms[i].triggered = true; // 置触发标志,防止重复触发 + alarm_trigger_action(i); // 触发当前闹钟(传索引) + + // 创建当前闹钟的独立停止任务(传索引作为参数) + BaseType_t ret = xTaskCreate((TaskFunction_t)alarm_stop_action, + "alarm_stop_task", // 任务名可加索引,比如"alarm_stop_0" + 2048, // 栈空间保留2048,足够用 + (void *)(uint32_t)i, // 传闹钟索引给停止任务 + 5, // 任务优先级,和原有一致 + &g_alarms[i].task_hdl); // 绑定当前闹钟的任务句柄 + if (ret != pdPASS) + { + ESP_LOGE(Clock, "ALARM[%d] Create stop task failed!", i); + g_alarms[i].triggered = false; // 创建失败重置标志 + g_alarms[i].task_hdl = NULL; + } + } + } +} + +/** + * @brief 初始化闹钟定时器(每秒检查一次,保留原有逻辑) + * @return esp_err_t 成功=ESP_OK + */ +static esp_err_t alarm_timer_init(void) +{ + esp_timer_create_args_t alarm_timer_args = { + .callback = &alarm_check_timer_cb, + .name = "alarm_check_timer"}; + ESP_ERROR_CHECK(esp_timer_create(&alarm_timer_args, &g_alarm_check_timer)); + return esp_timer_start_periodic(g_alarm_check_timer, 1000000); // 1秒检查一次 +} + +/** + * @brief 将闹钟配置保存到 NVS + */ +static void alarm_save_to_nvs(uint8_t alarm_idx) +{ + if (alarm_idx >= ALARM_MAX_NUM) + return; + + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) + { + ESP_LOGE(Clock, "Failed to open NVS: %s", esp_err_to_name(err)); + return; + } + + char key_time[32], key_enable[32]; + snprintf(key_time, sizeof(key_time), "alarm%d_time", alarm_idx); + snprintf(key_enable, sizeof(key_enable), "alarm%d_enable", alarm_idx); + + // 把时间编码为单个32位整数:(hour << 16) | (minute << 8) | second + uint32_t time_data = ((uint32_t)g_alarms[alarm_idx].hour << 16) | + ((uint32_t)g_alarms[alarm_idx].minute << 8) | + g_alarms[alarm_idx].second; + + nvs_set_u32(nvs_handle, key_time, time_data); + nvs_set_u8(nvs_handle, key_enable, g_alarms[alarm_idx].enable ? 1 : 0); + nvs_commit(nvs_handle); + nvs_close(nvs_handle); + + ESP_LOGI(Clock, "Alarm[%d] saved to NVS", alarm_idx); +} + +/** + * @brief 从 NVS 读取闹钟配置 + */ +static void alarm_load_from_nvs(uint8_t alarm_idx) +{ + if (alarm_idx >= ALARM_MAX_NUM) + return; + + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs_handle); + if (err != ESP_OK) + { + ESP_LOGE(Clock, "Failed to open NVS: %s", esp_err_to_name(err)); + return; + } + + char key_time[32], key_enable[32]; + snprintf(key_time, sizeof(key_time), "alarm%d_time", alarm_idx); + snprintf(key_enable, sizeof(key_enable), "alarm%d_enable", alarm_idx); + + uint32_t time_data = 0; + uint8_t enable_data = 0; + + if (nvs_get_u32(nvs_handle, key_time, &time_data) == ESP_OK) + { + g_alarms[alarm_idx].hour = (time_data >> 16) & 0xFF; + g_alarms[alarm_idx].minute = (time_data >> 8) & 0xFF; + g_alarms[alarm_idx].second = time_data & 0xFF; + } + + if (nvs_get_u8(nvs_handle, key_enable, &enable_data) == ESP_OK) + { + g_alarms[alarm_idx].enable = (enable_data != 0); + } + + nvs_close(nvs_handle); + + // 初始化窗帘打开标志为false,确保重新加载后状态正确 + g_alarms[alarm_idx].curtain_opened = false; + + ESP_LOGI(Clock, "Alarm[%d] loaded from NVS: %02d:%02d:%02d (enable=%d)", + alarm_idx, g_alarms[alarm_idx].hour, g_alarms[alarm_idx].minute, + g_alarms[alarm_idx].second, g_alarms[alarm_idx].enable); +} + +/** + * @brief 设置【指定闹钟】的时间(新增索引参数,适配多闹钟) + * @param alarm_idx 闹钟索引(0~ALARM_MAX_NUM-1) + * @param hour 时(0-23) + * @param minute 分(0-59) + * @param second 秒(0-59) + */ +void alarm_set_time(uint8_t alarm_idx, uint8_t hour, uint8_t minute, uint8_t second) +{ + // 索引合法性检查 + if (alarm_idx >= ALARM_MAX_NUM) + { + ESP_LOGE(Clock, "Set time idx %d is invalid! Max: %d", alarm_idx, ALARM_MAX_NUM - 1); + return; + } + // 时间合法性检查,保留原有 + if (hour > 23 || minute > 59 || second > 59) + { + ESP_LOGE(Clock, "ALARM[%d] Invalid time! %02d:%02d:%02d", alarm_idx, hour, minute, second); + return; + } + // 设置指定闹钟的时间,重置触发标志和窗帘打开标志 + g_alarms[alarm_idx].hour = hour; + g_alarms[alarm_idx].minute = minute; + g_alarms[alarm_idx].second = second; + g_alarms[alarm_idx].triggered = false; + g_alarms[alarm_idx].curtain_opened = false; // 重置窗帘打开标志 + + alarm_save_to_nvs(alarm_idx); // 保存到 NVS + + ESP_LOGI(Clock, "ALARM[%d] Time set to: %02d:%02d:%02d", alarm_idx, hour, minute, second); +} + +/** + * @brief 开启/关闭【指定闹钟】(新增索引参数,适配多闹钟) + * @param alarm_idx 闹钟索引(0~ALARM_MAX_NUM-1) + * @param enable true=开启,false=关闭 + */ +void alarm_set_enable(uint8_t alarm_idx, bool enable) +{ + // 索引合法性检查 + if (alarm_idx >= ALARM_MAX_NUM) + { + ESP_LOGE(Clock, "Set enable idx %d is invalid! Max: %d", alarm_idx, ALARM_MAX_NUM - 1); + return; + } + // 设置指定闹钟的使能状态,重置触发标志、窗帘打开标志和任务句柄 + g_alarms[alarm_idx].enable = enable; + g_alarms[alarm_idx].triggered = false; + g_alarms[alarm_idx].curtain_opened = false; // 重置窗帘打开标志 + g_alarms[alarm_idx].task_hdl = NULL; + + alarm_save_to_nvs(alarm_idx); // 保存到 NVS + + if (enable) + { + ESP_LOGI(Clock, "ALARM[%d] is ENABLED", alarm_idx); + } + else + { + ESP_LOGI(Clock, "ALARM[%d] is DISABLED", alarm_idx); + // 关闭时立即停止蜂鸣器,执行停止动作(保留原有逻辑) + buzzer_control_flag = false; + } +} + +// 【可选】新增:一键关闭所有闹钟(方便快捷操作) +void alarm_disable_all(void) +{ + for (uint8_t i = 0; i < ALARM_MAX_NUM; i++) + { + alarm_set_enable(i, false); + } + ESP_LOGI(Clock, "All alarms are DISABLED"); +} +// SNTP 初始化 +// =========================== SNTP 初始化 =========================== + +// =========================== 时间相关函数 =========================== +static void set_timezone(void) +{ + // 设置中国标准时间(北京时间) + setenv("TZ", "CST-8", 1); + tzset(); + ESP_LOGI(TAG, "时区设置为北京时间 (CST-8)"); +} + +static time_t get_current_time(void) +{ + // 使用POSIX函数获取时间 + return time(NULL); +} + +static void print_current_time(void) +{ + time_t now = get_current_time(); + struct tm timeinfo; + char buffer[64]; + + localtime_r(&now, &timeinfo); + strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S %A", &timeinfo); + + ESP_LOGI(TAG, "当前时间: %s", buffer); +} + +static void initialize_sntp(void) +{ + ESP_LOGI(TAG, "初始化SNTP服务"); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + // 设置时间服务器(默认使用 pool.ntp.org) + esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); + // 添加 NTP 服务器 + // esp_sntp_setservername(0, "pool.ntp.org"); // 默认服务器 + esp_sntp_setservername(0, "cn.pool.ntp.org"); // 中国 NTP 服务器 + esp_sntp_setservername(1, "ntp1.aliyun.com"); // 阿里云 NTP 服务器 + // 初始化 SNTP + esp_sntp_init(); +#else + sntp_setoperatingmode(SNTP_OPMODE_POLL); + sntp_setservername(0, "pool.ntp.org"); + sntp_setservername(1, "cn.pool.ntp.org"); + sntp_setservername(2, "ntp1.aliyun.com"); + sntp_init(); // 初始化 SNTP +#endif + + set_timezone(); + print_current_time(); +} + +/*---------------------------------------时间获取相关函数-END -----------------------------------------*/ + +/** + * @brief 更新遥测数据并上报 + * 辅助函数:同步控制标志到遥测数据结构并上报 + */ +static void update_telemetry_and_report(void) +{ + if (xMqttMessageMutex != NULL && xSemaphoreTake(xMqttMessageMutex, portMAX_DELAY) == pdTRUE) + { + // 同步窗帘状态 + strcpy(g_device_message.telemetry.curtain_state, servo_control_flag ? "open" : "close"); + // 同步灯光状态 + strcpy(g_device_message.telemetry.led_state, light_source_control_flag ? "open" : "close"); + g_device_message.telemetry.led_power = led_brightness_value; + // 同步风扇状态 + strcpy(g_device_message.telemetry.fan_state, fan_control_flag ? "open" : "close"); + // 同步蜂鸣器状态 + strcpy(g_device_message.telemetry.buzzer_state, buzzer_control_flag ? "open" : "close"); + xSemaphoreGive(xMqttMessageMutex); + } + // 立即上报 + mqtt_publish_feedback(); +} + +/** + * @brief 【预留】白天模式事件执行 + * 根据光照强度判断是否关闭窗帘和是否打开LED灯 + * 光照强度阈值: + * - 光照充足 (>500 lux):关闭窗帘,关闭灯光 + * - 光照不足 (<200 lux):打开窗帘,打开灯光 + */ +static void on_day_period_event(void) +{ + ESP_LOGI(TIME_PERIOD_TAG, "=== DAY PERIOD EVENT TRIGGERED ==="); + + // 获取当前光照强度 + float current_lux = 0; + bool lux_valid = false; + + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + current_lux = g_sensor_data.lux; + lux_valid = g_sensor_data.bh1750_valid; + xSemaphoreGive(xSensorDataMutex); + } + + if (!lux_valid) + { + ESP_LOGW(TIME_PERIOD_TAG, "Light sensor data invalid, skipping day mode auto control"); + return; + } + + ESP_LOGI(TIME_PERIOD_TAG, "Current light intensity: %.2f lux", current_lux); + + // 根据光照强度执行控制逻辑 + if (current_lux > 500.0f) + { + // 光照充足:关闭窗帘,关闭灯光 + servo_control_flag = false; + light_source_control_flag = false; + led_brightness_value = 0; + ESP_LOGI(TIME_PERIOD_TAG, "Day mode: Light sufficient (>500 lux), closing curtain and turning off light"); + } + else if (current_lux < 200.0f) + { + // 光照不足:打开窗帘,打开灯光 + servo_control_flag = true; + light_source_control_flag = true; + led_brightness_value = 80; // 白天亮度较高 + ESP_LOGI(TIME_PERIOD_TAG, "Day mode: Light insufficient (<200 lux), opening curtain and turning on light"); + } + else + { + // 光照适中:保持当前状态或打开窗帘(让更多自然光进入) + servo_control_flag = true; + ESP_LOGI(TIME_PERIOD_TAG, "Day mode: Light moderate (200-500 lux), opening curtain for natural light"); + } + + // 更新遥测数据并上报 + update_telemetry_and_report(); +} + +/** + * @brief 【预留】晚上模式事件执行 + * 在此处添加晚上模式触发时需要执行的操作 + */ +static void on_night_period_event(void) +{ + ESP_LOGI(TIME_PERIOD_TAG, "=== NIGHT PERIOD EVENT TRIGGERED ==="); + + // 关闭窗帘(舵机控制) + servo_control_flag = false; + ESP_LOGI(TIME_PERIOD_TAG, "Night mode: Closing curtain"); + + // 打开灯(设置灯源控制标志和亮度值) + light_source_control_flag = true; + led_brightness_value = 50; // 设置为50%亮度,可根据需要调整 + ESP_LOGI(TIME_PERIOD_TAG, "Night mode: Turning on light (brightness=%d)", led_brightness_value); + + // 更新遥测数据并上报 + update_telemetry_and_report(); +} + +#define I2C_MASTER_SCL_IO 5 // GPIO number for I2C master clock +#define I2C_MASTER_SDA_IO 4 // GPIO number for I2C master data +#define I2C_MASTER_NUM I2C_NUM_0 // I2C port number for master dev +#define I2C_MASTER_FREQ_HZ 100000 // I2C master clock frequency + +#define SERVO_GPIO (10) // Servo GPIO + +// ========================= MQTT配置 ========================= +#define MQTT_BROKER_URL "mqtt://beihong.wang:1883" // MQTT代理URL,从menuconfig配置获取 +#define MQTT_USERNAME "esp_mqtt_client" // MQTT用户名 +#define MQTT_CLIENT_ID "esp_mqtt_client" // MQTT客户端ID +#define MQTT_PASSWORD "664hd78gas97" // MQTT密码 + +// MQTT主题配置 +#define MQTT_PUBLISH_TOPIC_QOS0 "topic/sensor/esp32_iothome_001" // QoS0发布的主题 +#define MQTT_NOTIFY_TOPIC "topic/control/esp32_iothome_001" // 上层通知主题 +#define MQTT_UNSUBSCRIBE_TOPIC "topic/control/esp32_iothome_001" // 取消订阅的主题 + +// 添加MQTT发布任务函数 +void mqtt_publish_task(void *pvParameters) +{ + TickType_t last_wake_time = xTaskGetTickCount(); + + // 初始化设备消息 + if (xMqttMessageMutex != NULL && xSemaphoreTake(xMqttMessageMutex, portMAX_DELAY) == pdTRUE) + { + // 设置设备ID和类型 + strcpy(g_device_message.device_id, "esp32_bedroom_001"); + strcpy(g_device_message.device_type, "bedroom_controller"); + + // 使用SNTP同步的时间作为初始时间戳 + time_t now; + time(&now); + g_device_message.timestamp = (uint64_t)(now * 1000ULL); // 转换为毫秒时间戳 + + // 初始化设备状态 + g_device_message.state.online = true; + strcpy(g_device_message.state.current_mode, "night_mode"); + g_device_message.state.home_status = true; + g_device_message.state.standby_mode = false; + g_device_message.state.error_code = 0; + + // 初始化遥测数据(默认值) + g_device_message.telemetry.temperature = 0; + g_device_message.telemetry.humidity = 0; + g_device_message.telemetry.light_intensity = 0; + g_device_message.telemetry.air_quality = 0; + strcpy(g_device_message.telemetry.curtain_state, "close"); + strcpy(g_device_message.telemetry.led_state, "close"); + g_device_message.telemetry.led_power = 0; + strcpy(g_device_message.telemetry.fan_state, "close"); + strcpy(g_device_message.telemetry.buzzer_state, "close"); + + xSemaphoreGive(xMqttMessageMutex); + } + + while (1) + { + // 更新传感器数据 + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + if (xMqttMessageMutex != NULL && xSemaphoreTake(xMqttMessageMutex, portMAX_DELAY) == pdTRUE) + { + // 更新温湿度数据 + if (g_sensor_data.ahtxx_valid) + { + g_device_message.telemetry.temperature = g_sensor_data.temperature; + g_device_message.telemetry.humidity = g_sensor_data.humidity; + } + + // 更新光照强度数据 + if (g_sensor_data.bh1750_valid) + { + g_device_message.telemetry.light_intensity = g_sensor_data.lux; + } + + // 更新空气质量数据 + if (g_sensor_data.mq135_valid) + { + g_device_message.telemetry.air_quality = g_sensor_data.air_quality; + } + + xSemaphoreGive(xMqttMessageMutex); + } + xSemaphoreGive(xSensorDataMutex); + } + + // 构建JSON消息 + char *json_message = NULL; + if (xMqttMessageMutex != NULL && xSemaphoreTake(xMqttMessageMutex, portMAX_DELAY) == pdTRUE) + { + // 更新时间戳为当前时间,确保使用同步后的时间 + time_t now; + time(&now); + g_device_message.timestamp = (uint64_t)(now * 1000ULL); // 转换为毫秒时间戳 + + cJSON *root = cJSON_CreateObject(); + cJSON_AddStringToObject(root, "type", "device_message"); + cJSON_AddStringToObject(root, "device_id", g_device_message.device_id); + cJSON_AddStringToObject(root, "device_type", g_device_message.device_type); + cJSON_AddNumberToObject(root, "timestamp", (double)g_device_message.timestamp); + cJSON_AddStringToObject(root, "message_type", "sensor_data"); + + cJSON *data_obj = cJSON_CreateObject(); + cJSON_AddItemToObject(root, "data", data_obj); + + cJSON *state_obj = cJSON_CreateObject(); + cJSON_AddBoolToObject(state_obj, "online", g_device_message.state.online); + cJSON_AddStringToObject(state_obj, "current_mode", g_device_message.state.current_mode); + // 注意:根据项目规范,不包含 home_status 字段以保持与现有上位机的兼容性 + cJSON_AddBoolToObject(state_obj, "standby_mode", g_device_message.state.standby_mode); + cJSON_AddNumberToObject(state_obj, "error_code", g_device_message.state.error_code); + cJSON_AddItemToObject(data_obj, "state", state_obj); + + // 创建遥测对象并使用格式化字符串确保两位小数 + cJSON *telemetry_obj = cJSON_CreateObject(); + + cJSON_AddNumberToObject(telemetry_obj, "temperature", roundf(g_device_message.telemetry.temperature * 100) / 100); + cJSON_AddNumberToObject(telemetry_obj, "humidity", roundf(g_device_message.telemetry.humidity * 100) / 100); + cJSON_AddNumberToObject(telemetry_obj, "light_intensity", roundf(g_device_message.telemetry.light_intensity * 100) / 100); + + cJSON_AddNumberToObject(telemetry_obj, "air_quality", g_device_message.telemetry.air_quality); + cJSON_AddStringToObject(telemetry_obj, "curtain_state", g_device_message.telemetry.curtain_state); + cJSON_AddStringToObject(telemetry_obj, "led_state", g_device_message.telemetry.led_state); + cJSON_AddNumberToObject(telemetry_obj, "led_power", g_device_message.telemetry.led_power); + cJSON_AddStringToObject(telemetry_obj, "fan_state", g_device_message.telemetry.fan_state); + cJSON_AddStringToObject(telemetry_obj, "buzzer_state", g_device_message.telemetry.buzzer_state); + cJSON_AddItemToObject(data_obj, "telemetry", telemetry_obj); // 添加遥测数据对象 + + json_message = cJSON_Print(root); + cJSON_Delete(root); + + xSemaphoreGive(xMqttMessageMutex); + } + + if (json_message != NULL) + { + ESP_LOGI(TAG, "准备发布MQTT消息:"); + ESP_LOGI(TAG, "Topic: %s", MQTT_PUBLISH_TOPIC_QOS0); + // ESP_LOGI(TAG, "Message: %s", json_message); + + // 发布MQTT消息 + if (g_mqtt_client != NULL) + { + int msg_id = esp_mqtt_client_publish(g_mqtt_client, MQTT_PUBLISH_TOPIC_QOS0, json_message, 0, 0, 0); + ESP_LOGI(TAG, "MQTT消息已发布, msg_id=%d", msg_id); + } + + // 释放内存 + free(json_message); + } + + // 根据home_status状态决定延时时间 + uint32_t delay_ms = 3000; // 默认3秒 + if (xMqttMessageMutex != NULL && xSemaphoreTake(xMqttMessageMutex, 10) == pdTRUE) + { + if (!g_device_message.state.home_status) + { + delay_ms = 50000; // 如果home_status为false,则50秒 + } + xSemaphoreGive(xMqttMessageMutex); + } + + // 按照设定的延时发布 + vTaskDelayUntil(&last_wake_time, pdMS_TO_TICKS(delay_ms)); + } +} + +void mqtt_publish_feedback(void) +{ + if (g_mqtt_client == NULL) + return; + + if (xMqttMessageMutex != NULL && xSemaphoreTake(xMqttMessageMutex, portMAX_DELAY) == pdTRUE) + { + // 更新时间戳 + time_t now; + time(&now); + g_device_message.timestamp = (uint64_t)(now * 1000ULL); + + // 构建JSON + cJSON *root = cJSON_CreateObject(); + cJSON_AddStringToObject(root, "type", "device_message"); + cJSON_AddStringToObject(root, "device_id", g_device_message.device_id); + cJSON_AddStringToObject(root, "device_type", g_device_message.device_type); + cJSON_AddNumberToObject(root, "timestamp", (double)g_device_message.timestamp); + cJSON_AddStringToObject(root, "message_type", "control_feedback"); + + cJSON *data_obj = cJSON_CreateObject(); + cJSON_AddItemToObject(root, "data", data_obj); + + cJSON *state_obj = cJSON_CreateObject(); + cJSON_AddBoolToObject(state_obj, "online", g_device_message.state.online); + cJSON_AddStringToObject(state_obj, "current_mode", g_device_message.state.current_mode); + // 注意:根据项目规范,不包含 home_status 字段以保持与现有上位机的兼容性 + cJSON_AddBoolToObject(state_obj, "standby_mode", g_device_message.state.standby_mode); + cJSON_AddNumberToObject(state_obj, "error_code", g_device_message.state.error_code); + cJSON_AddItemToObject(data_obj, "state", state_obj); + + // 添加遥测和状态 + cJSON *telemetry_obj = cJSON_CreateObject(); + cJSON_AddStringToObject(telemetry_obj, "fan_state", g_device_message.telemetry.fan_state); + cJSON_AddStringToObject(telemetry_obj, "led_state", g_device_message.telemetry.led_state); + cJSON_AddNumberToObject(telemetry_obj, "led_power", g_device_message.telemetry.led_power); + cJSON_AddStringToObject(telemetry_obj, "curtain_state", g_device_message.telemetry.curtain_state); + cJSON_AddStringToObject(telemetry_obj, "buzzer_state", g_device_message.telemetry.buzzer_state); + cJSON_AddItemToObject(data_obj, "telemetry", telemetry_obj); + + char *json_message = cJSON_Print(root); + cJSON_Delete(root); + + if (json_message) + { + esp_mqtt_client_publish(g_mqtt_client, MQTT_PUBLISH_TOPIC_QOS0, json_message, 0, 0, 0); + free(json_message); + } + + xSemaphoreGive(xMqttMessageMutex); + } +} + +/** + * @brief Event handler registered to receive MQTT events + * + * This function is called by the MQTT client event loop. + * + * @param handler_args user data registered to the event. + * @param base Event base for the handler (always MQTT Base in this example). + * @param event_id The id for the received event. + * @param event_data The data for the event, esp_mqtt_event_handle_t. + */ +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"); + // 订阅主题 + msg_id = esp_mqtt_client_subscribe(client, MQTT_NOTIFY_TOPIC, 2); + ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); + 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); + // 订阅成功后发布消息 + msg_id = esp_mqtt_client_publish(client, MQTT_PUBLISH_TOPIC_QOS0, "data", 0, 0, 0); + ESP_LOGI(TAG, "sent publish successful, msg_id=%d", 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); + + // 检查是否是控制主题 + if (strncmp(event->topic, MQTT_NOTIFY_TOPIC, event->topic_len) == 0) + { + // 解析JSON数据 + cJSON *root = cJSON_ParseWithLength(event->data, event->data_len); + if (root == NULL) + { + ESP_LOGE(TAG, "Failed to parse JSON data"); + break; + } + + // 检查消息类型 + cJSON *type_obj = cJSON_GetObjectItem(root, "type"); + if (type_obj != NULL && cJSON_IsString(type_obj) && strcmp(type_obj->valuestring, "status") == 0) + { + // 处理 status 类型消息(例如其它客户端上线通知),收到 online 则设置设备在线并立即上报 + cJSON *subtype_obj = cJSON_GetObjectItem(root, "subtype"); + cJSON *status_obj = cJSON_GetObjectItem(root, "status"); + const char *status_str = NULL; + if (subtype_obj != NULL && cJSON_IsString(subtype_obj)) + { + status_str = cJSON_GetStringValue(subtype_obj); + } + else if (status_obj != NULL && cJSON_IsString(status_obj)) + { + status_str = cJSON_GetStringValue(status_obj); + } + + if (status_str != NULL && strcmp(status_str, "online") == 0) + { + ESP_LOGI(TAG, "Received status online message, starting reporting"); + if (xMqttMessageMutex != NULL && xSemaphoreTake(xMqttMessageMutex, portMAX_DELAY) == pdTRUE) + { + g_device_message.state.online = true; + /* 将 home_status 置为 true 以恢复较短的上报间隔(原逻辑使用 home_status 控制上报频率) */ + g_device_message.state.home_status = true; + xSemaphoreGive(xMqttMessageMutex); + } + + // 立即反馈/上报一次状态和遥测 + mqtt_publish_feedback(); + } + + cJSON_Delete(root); + break; + } + + // 普通控制消息继续走原有处理流程 + + // 检查设备ID和类型是否匹配 + + if (type_obj == NULL || !cJSON_IsString(type_obj)) + { + ESP_LOGE(TAG, "Invalid message type"); + cJSON_Delete(root); + break; + } + + // 检查设备ID和类型是否匹配 + cJSON *device_id_obj = cJSON_GetObjectItem(root, "device_id"); + cJSON *device_type_obj = cJSON_GetObjectItem(root, "device_type"); + if (device_id_obj == NULL || !cJSON_IsString(device_id_obj) || + device_type_obj == NULL || !cJSON_IsString(device_type_obj)) + { + ESP_LOGE(TAG, "Missing device identification"); + cJSON_Delete(root); + break; + } + + if (strcmp(cJSON_GetStringValue(device_id_obj), "esp32_bedroom_001") != 0 || + strcmp(cJSON_GetStringValue(device_type_obj), "bedroom_controller") != 0) + { + ESP_LOGE(TAG, "Device identification does not match"); + cJSON_Delete(root); + break; + } + + // 检查消息类型是否为控制命令 + if (strcmp(type_obj->valuestring, "control_command") == 0) + { + // 获取request_id并存储 + cJSON *request_id_obj = cJSON_GetObjectItem(root, "request_id"); + if (request_id_obj != NULL && cJSON_IsString(request_id_obj)) + { + // 这里可以存储request_id以备后续使用 + ESP_LOGI(TAG, "Received request_id: %s", request_id_obj->valuestring); + } + + // 检查消息子类型 + cJSON *message_type_obj = cJSON_GetObjectItem(root, "message_type"); + if (message_type_obj != NULL && cJSON_IsString(message_type_obj)) + { + const char *message_type = cJSON_GetStringValue(message_type_obj); + + // 处理presence_request消息类型 + if (strcmp(message_type, "presence_request") == 0) + { + cJSON *data_obj = cJSON_GetObjectItem(root, "data"); + if (data_obj != NULL && cJSON_IsObject(data_obj)) + { + cJSON *presence_obj = cJSON_GetObjectItem(data_obj, "presence"); + if (presence_obj != NULL && cJSON_IsString(presence_obj)) + { + const char *presence_status = cJSON_GetStringValue(presence_obj); + ESP_LOGI(TAG, "Received presence status: %s", presence_status); + + // 更新设备在家状态 + if (xMqttMessageMutex != NULL && xSemaphoreTake(xMqttMessageMutex, portMAX_DELAY) == pdTRUE) + { + if (strcmp(presence_status, "home") == 0) + { + g_device_message.state.home_status = true; + // 将背光的标志位置置设置为true,表示设备在家 + led_backlight_on = true; + ESP_LOGI(TAG, "Device is present at home"); + } + else if (strcmp(presence_status, "away") == 0) + { + g_device_message.state.home_status = false; + // 将背光的标志位置置设置为false,表示设备离家 + led_backlight_on = false; + ESP_LOGI(TAG, "Device is away from home"); + } + + xSemaphoreGive(xMqttMessageMutex); + } + } + } + } + // 如果不是presence_request,则按原来的逻辑处理 + else + { + // 获取controls对象 + cJSON *data_obj = cJSON_GetObjectItem(root, "data"); + if (data_obj != NULL && cJSON_IsObject(data_obj)) + { + cJSON *controls_obj = cJSON_GetObjectItem(data_obj, "controls"); + if (controls_obj != NULL && cJSON_IsObject(controls_obj)) + { + ESP_LOGI(TAG, "Processing control command"); + + // 解析controls中的各个控制项 + + // 解析风扇控制 + cJSON *fan_power_obj = cJSON_GetObjectItem(controls_obj, "fan_state"); + if (fan_power_obj != NULL && cJSON_IsString(fan_power_obj)) + { + const char *fan_power_str = cJSON_GetStringValue(fan_power_obj); + + if (strcmp(fan_power_str, "open") == 0) + { + strcpy(g_device_message.telemetry.fan_state, "open"); + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + fan_control_flag = true; + xSemaphoreGive(xControlFlagMutex); + } + // gpio_set_level(GPIO_NUM_1, 1); // 打开风扇 + ESP_LOGI(TAG, "Fan turned OPEN"); + } + else if (strcmp(fan_power_str, "close") == 0 || strcmp(fan_power_str, "closed") == 0) + { + strcpy(g_device_message.telemetry.fan_state, "close"); + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + fan_control_flag = false; + xSemaphoreGive(xControlFlagMutex); + } + // gpio_set_level(GPIO_NUM_1, 0); // 关闭风扇 + ESP_LOGI(TAG, "Fan turned CLOSE"); + } + else + { + ESP_LOGW(TAG, "Invalid fan command: %s", fan_power_str); + } + + mqtt_publish_feedback(); // 立即反馈 + } + + // 解析窗帘控制 + cJSON *curtain_obj = cJSON_GetObjectItem(controls_obj, "curtain_state"); + if (curtain_obj != NULL && cJSON_IsString(curtain_obj)) + { + const char *curtain_str = cJSON_GetStringValue(curtain_obj); + + if (strcmp(curtain_str, "open") == 0) + { + strcpy(g_device_message.telemetry.curtain_state, "open"); + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + servo_control_flag = true; + xSemaphoreGive(xControlFlagMutex); + } + // servo_set_angle(180); // 打开窗帘 + ESP_LOGI(TAG, "Curtain opened"); + } + else if (strcmp(curtain_str, "close") == 0 || strcmp(curtain_str, "closed") == 0) + { + strcpy(g_device_message.telemetry.curtain_state, "close"); + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + servo_control_flag = false; + xSemaphoreGive(xControlFlagMutex); + } + // servo_set_angle(0); // 关闭窗帘 + ESP_LOGI(TAG, "Curtain closed"); + } + else + { + ESP_LOGW(TAG, "Invalid curtain command: %s", curtain_str); + } + + mqtt_publish_feedback(); // 立即反馈 + } + + // 解析蜂鸣器控制 + cJSON *buzzer_power_obj = cJSON_GetObjectItem(controls_obj, "buzzer_state"); + if (buzzer_power_obj != NULL && cJSON_IsString(buzzer_power_obj)) + { + const char *buzzer_power_str = cJSON_GetStringValue(buzzer_power_obj); + + if (strcmp(buzzer_power_str, "open") == 0) + { + strcpy(g_device_message.telemetry.buzzer_state, "open"); + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + buzzer_control_flag = true; + xSemaphoreGive(xControlFlagMutex); + } + // sendControlFrame(0x02, 1); // 打开蜂鸣器 + ESP_LOGI(TAG, "Buzzer turned OPEN"); + } + else if (strcmp(buzzer_power_str, "close") == 0 || strcmp(buzzer_power_str, "closed") == 0) + { + strcpy(g_device_message.telemetry.buzzer_state, "close"); + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + buzzer_control_flag = false; + xSemaphoreGive(xControlFlagMutex); + } + // sendControlFrame(0x02, 0); // 关闭蜂鸣器 + ESP_LOGI(TAG, "Buzzer turned CLOSE"); + } + else + { + ESP_LOGW(TAG, "Invalid buzzer command: %s", buzzer_power_str); + } + + mqtt_publish_feedback(); // 立即反馈 + } + + // 解析LED控制 + cJSON *led_state_obj = cJSON_GetObjectItem(controls_obj, "led_state"); + cJSON *led_power_obj = cJSON_GetObjectItem(controls_obj, "led_power"); + bool led_changed = false; + + if (led_power_obj != NULL && cJSON_IsNumber(led_power_obj)) + { + int power = cJSON_GetNumberValue(led_power_obj); + if (power < 0) + power = 0; + if (power > 100) + power = 100; + + g_device_message.telemetry.led_power = (uint8_t)power; + strcpy(g_device_message.telemetry.led_state, power > 0 ? "open" : "close"); + + // 更新全局LED亮度值和状态 + led_brightness_value = (uint8_t)power; + light_source_control_flag = (power > 0); + + ESP_LOGI(TAG, "LED power set to %d", power); + led_changed = true; + } + else if (led_state_obj != NULL && cJSON_IsString(led_state_obj)) + { + const char *state = cJSON_GetStringValue(led_state_obj); + + if (strcmp(state, "open") == 0) + { + strcpy(g_device_message.telemetry.led_state, "open"); + g_device_message.telemetry.led_power = 100; + + // 更新全局LED亮度值和状态 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + led_brightness_value = 100; + light_source_control_flag = true; + xSemaphoreGive(xControlFlagMutex); + } + + ESP_LOGI(TAG, "LED turned OPEN"); + led_changed = true; + } + else if (strcmp(state, "close") == 0 || strcmp(state, "closed") == 0) + { + strcpy(g_device_message.telemetry.led_state, "close"); + g_device_message.telemetry.led_power = 0; + + // 更新全局LED亮度值和状态 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + led_brightness_value = 0; + light_source_control_flag = false; + xSemaphoreGive(xControlFlagMutex); + } + + ESP_LOGI(TAG, "LED turned CLOSE"); + led_changed = true; + } + else + { + ESP_LOGW(TAG, "Invalid LED command: %s", state); + } + } + + // 一次反馈 + if (led_changed) + { + mqtt_publish_feedback(); + } + + // 解析风扇功率值控制 (数值) - 已移除此功能 + // 保留此段注释作为参考,实际代码中无需实现该功能 + + // 可以在此处添加其他控制项的解析 + // 如led_power, curtain等 + + // 解析闹钟设置 (alarm1_time/alarm1_enable ... alarm3_time/alarm3_enable) + for (int ai = 1; ai <= ALARM_MAX_NUM; ai++) + { + char key_time[32]; + char key_enable[32]; + snprintf(key_time, sizeof(key_time), "alarm%d_time", ai); + snprintf(key_enable, sizeof(key_enable), "alarm%d_enable", ai); + + cJSON *alarm_time_obj = cJSON_GetObjectItem(controls_obj, key_time); + if (alarm_time_obj != NULL && cJSON_IsString(alarm_time_obj)) + { + const char *time_str = cJSON_GetStringValue(alarm_time_obj); + int hh = 0, mm = 0, ss = 0; + int parts = 0; + // 支持 HH:MM 或 HH:MM:SS 格式 + parts = sscanf(time_str, "%d:%d:%d", &hh, &mm, &ss); + if (parts >= 2) + { + if (parts == 2) + ss = 0; + // 调用 alarm_set_time,index 从0开始 + ESP_LOGI(TAG, "Set alarm%d time to %02d:%02d:%02d", ai, hh, mm, ss); + alarm_set_time(ai - 1, hh, mm, ss); + // 启用闹钟(如果没有专门的 enable 字段,保持原有状态) + } + } + + cJSON *alarm_enable_obj = cJSON_GetObjectItem(controls_obj, key_enable); + if (alarm_enable_obj != NULL && cJSON_IsString(alarm_enable_obj)) + { + const char *enable_str = cJSON_GetStringValue(alarm_enable_obj); + bool enable = false; + if (strcasecmp(enable_str, "on") == 0 || strcasecmp(enable_str, "true") == 0 || strcasecmp(enable_str, "enable") == 0) + enable = true; + else + enable = false; + + ESP_LOGI(TAG, "Set alarm%d enable = %s", ai, enable ? "true" : "false"); + alarm_set_enable(ai - 1, enable); + } + + // 如果任一 alarm 字段存在则发送一次反馈,告知上位机当前设置 + if ((alarm_time_obj != NULL && cJSON_IsString(alarm_time_obj)) || (alarm_enable_obj != NULL && cJSON_IsString(alarm_enable_obj))) + { + mqtt_publish_feedback(); + } + } + + // 解析时间段设置 (白天模式和晚上模式) + cJSON *day_period_start_obj = cJSON_GetObjectItem(controls_obj, "day_period_start"); + cJSON *day_period_end_obj = cJSON_GetObjectItem(controls_obj, "day_period_end"); + cJSON *night_period_start_obj = cJSON_GetObjectItem(controls_obj, "night_period_start"); + cJSON *night_period_end_obj = cJSON_GetObjectItem(controls_obj, "night_period_end"); + + bool period_changed = false; + + // 解析白天时间段 + if (day_period_start_obj != NULL && cJSON_IsString(day_period_start_obj) && + day_period_end_obj != NULL && cJSON_IsString(day_period_end_obj)) + { + const char *start_str = cJSON_GetStringValue(day_period_start_obj); + const char *end_str = cJSON_GetStringValue(day_period_end_obj); + int start_hh = 0, start_mm = 0, end_hh = 0, end_mm = 0; + + if (sscanf(start_str, "%d:%d", &start_hh, &start_mm) >= 2 && + sscanf(end_str, "%d:%d", &end_hh, &end_mm) >= 2) + { + ESP_LOGI(TAG, "Set day period: %02d:%02d - %02d:%02d", + start_hh, start_mm, end_hh, end_mm); + time_period_set(TIME_PERIOD_DAY, start_hh, start_mm, end_hh, end_mm); + period_changed = true; + } + } + + // 解析晚上时间段 + if (night_period_start_obj != NULL && cJSON_IsString(night_period_start_obj) && + night_period_end_obj != NULL && cJSON_IsString(night_period_end_obj)) + { + const char *start_str = cJSON_GetStringValue(night_period_start_obj); + const char *end_str = cJSON_GetStringValue(night_period_end_obj); + int start_hh = 0, start_mm = 0, end_hh = 0, end_mm = 0; + + if (sscanf(start_str, "%d:%d", &start_hh, &start_mm) >= 2 && + sscanf(end_str, "%d:%d", &end_hh, &end_mm) >= 2) + { + ESP_LOGI(TAG, "Set night period: %02d:%02d - %02d:%02d", + start_hh, start_mm, end_hh, end_mm); + time_period_set(TIME_PERIOD_NIGHT, start_hh, start_mm, end_hh, end_mm); + period_changed = true; + } + } + + // 时间段设置改变后发送反馈 + if (period_changed) + { + mqtt_publish_feedback(); + } + + // 解析降温模式温度阈值设置 + cJSON *temp_threshold_obj = cJSON_GetObjectItem(controls_obj, "temperature_threshold"); + if (temp_threshold_obj != NULL && cJSON_IsNumber(temp_threshold_obj)) + { + float threshold = (float)cJSON_GetNumberValue(temp_threshold_obj); + // 限制温度阈值范围:10-40°C + if (threshold < 10.0f) + threshold = 10.0f; + if (threshold > 40.0f) + threshold = 40.0f; + + g_temperature_threshold = threshold; + g_cooling_mode_enabled = true; + cooling_mode_save_to_nvs(); + ESP_LOGI(TAG, "Cooling mode temperature threshold set to %.1f°C", g_temperature_threshold); + mqtt_publish_feedback(); + } + } + } + } + } + cJSON_Delete(root); + } + break; + case MQTT_EVENT_ERROR: + ESP_LOGI(TAG, "MQTT_EVENT_ERROR"); + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) + { + ESP_LOGI(TAG, "Last error code reported from esp-tls: 0x%x", event->error_handle->esp_tls_last_esp_err); + ESP_LOGI(TAG, "Last tls stack error number: 0x%x", event->error_handle->esp_tls_stack_err); + ESP_LOGI(TAG, "Last captured errno : %d (%s)", event->error_handle->esp_transport_sock_errno, + strerror(event->error_handle->esp_transport_sock_errno)); + } + else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) + { + ESP_LOGI(TAG, "Connection refused error: 0x%x", event->error_handle->connect_return_code); + } + else + { + ESP_LOGW(TAG, "Unknown error type: 0x%x", event->error_handle->error_type); + } + break; + + default: + ESP_LOGI(TAG, "Other event id:%d", event->event_id); + break; + } + } +} + +// 修改mqtt_app_start函数,保存客户端句柄并创建MQTT发布任务 +static void mqtt_app_start(void) +{ + // 生成基于MAC地址的唯一客户端ID + char client_id[32]; + uint8_t mac[6]; + esp_read_mac(mac, ESP_MAC_WIFI_STA); + sprintf(client_id, "esp32_%02x%02x%02x", mac[3], mac[4], mac[5]); + + ESP_LOGI(TAG, "Generated Client ID: %s", client_id); + + // MQTT客户端配置 + esp_mqtt_client_config_t mqtt_cfg = { + .broker.address.uri = MQTT_BROKER_URL, // MQTT代理地址 + .credentials.username = MQTT_USERNAME, // 用户名 + .credentials.client_id = client_id, // 客户端ID + .credentials.authentication.password = MQTT_PASSWORD // 密码 + }; + +#if CONFIG_BROKER_URL_FROM_STDIN + char line[128]; + if (strcmp(mqtt_cfg.broker.address.uri, "FROM_STDIN") == 0) + { + int count = 0; + printf("请输入mqtt代理的url\n"); + while (count < 128) + { + int c = fgetc(stdin); + if (c == '\n') + { + line[count] = '\0'; + break; + } + else if (c > 0 && c < 127) + { + line[count] = c; + ++count; + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } + mqtt_cfg.broker.address.uri = line; + printf("代理url: %s\n", line); + } + else + { + ESP_LOGE(TAG, "配置不匹配: 错误的代理url"); + abort(); + } +#endif /* CONFIG_BROKER_URL_FROM_STDIN */ + + esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); + /* 最后一个参数可用于传递数据给事件处理程序,在此示例中为mqtt_event_handler */ + esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL); + esp_mqtt_client_start(client); + + // 保存MQTT客户端句柄到全局变量 + g_mqtt_client = client; + + // 创建MQTT发布任务 + xTaskCreate(mqtt_publish_task, "mqtt_publish_task", 4096, NULL, 5, NULL); +} + +// ========================= HTTP服务器相关函数 ========================= + +// I2C句柄已在前面声明 +extern i2c_master_bus_handle_t bus_handle; +extern i2c_master_dev_handle_t dev_handle; + +// 舵机初始化函数 +static void servo_init(void) +{ + ESP_LOGI(TAG, "初始化舵机控制"); + + // 配置舵机 + servo_config_t servo_cfg = { + .max_angle = 180, + .min_width_us = 500, + .max_width_us = 2500, + .freq = 50, + .timer_number = LEDC_TIMER_0, + .channels = { + .servo_pin = { + SERVO_GPIO, + }, + .ch = { + LEDC_CHANNEL_0, + }, + }, + .channel_number = 1, + }; + + // 初始化舵机 + esp_err_t ret = iot_servo_init(LEDC_LOW_SPEED_MODE, &servo_cfg); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "舵机初始化失败: %s", esp_err_to_name(ret)); + } + else + { + ESP_LOGI(TAG, "舵机初始化成功"); + } +} + +static esp_err_t i2c_master_init(void) +{ + i2c_master_bus_config_t i2c_mst_config = { + .clk_source = I2C_CLK_SRC_DEFAULT, + .i2c_port = I2C_MASTER_NUM, + .scl_io_num = I2C_MASTER_SCL_IO, + .sda_io_num = I2C_MASTER_SDA_IO, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = true, // 启用内部上拉电阻 + }; + + ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_mst_config, &bus_handle)); + + ESP_LOGI(TAG, "I2C master bus initialized successfully"); + return ESP_OK; +} + +void i2c0_ahtxx_task(void *pvParameters) +{ + // 定义AHT传感器配置,使用宏进行初始化 + ahtxx_config_t ahtxx_config = I2C_AHT30_CONFIG_DEFAULT; // 使用AHT30作为默认配置 + ahtxx_handle_t ahtxx_handle = NULL; + + // 尝试初始化AHT传感器 + esp_err_t ret = ahtxx_init(bus_handle, &ahtxx_config, &ahtxx_handle); + if (ret != ESP_OK) + { + ESP_LOGE(AHT30_TAG, "AHTxx初始化失败,任务将继续运行但传感器数据无效,错误码: 0x%x", ret); + // 不返回,而是进入无限循环,保持传感器无效状态 + while (1) + { + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + g_sensor_data.ahtxx_valid = false; + xSemaphoreGive(xSensorDataMutex); + } + vTaskDelay(pdMS_TO_TICKS(5000)); // 每5秒重试一次 + } + } + + // 循环读取数据 + float temperature, humidity; + while (1) + { + // 读取温湿度 + ret = ahtxx_get_measurement(ahtxx_handle, &temperature, &humidity); + if (ret != ESP_OK) + { + ESP_LOGE(AHT30_TAG, "读取温湿度失败,错误码: 0x%x", ret); + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + g_sensor_data.ahtxx_valid = false; + xSemaphoreGive(xSensorDataMutex); + } + } + else + { + // 更新全局传感器数据 + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + g_sensor_data.temperature = temperature; + g_sensor_data.humidity = humidity; + g_sensor_data.ahtxx_valid = true; + xSemaphoreGive(xSensorDataMutex); + } + + // ESP_LOGI(AHT30_TAG, "温度: %.2f °C, 湿度: %.2f %%", temperature, humidity); + } + + // 更新UI显示 - 需要获取其他传感器数据以更新完整显示 + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + ui_update_sensor_data( + g_sensor_data.ahtxx_valid ? g_sensor_data.temperature : -1.0f, + g_sensor_data.ahtxx_valid ? g_sensor_data.humidity : -1.0f, + g_sensor_data.bh1750_valid ? g_sensor_data.lux : -1.0f, + g_sensor_data.mq135_valid ? g_sensor_data.air_quality : -1.0f, + g_sensor_data.mq135_valid ? (g_sensor_data.air_quality <= 20 ? "Excellent" : g_sensor_data.air_quality <= 100 ? "Good" + : g_sensor_data.air_quality <= 300 ? "Moderate" + : "High") + : "N/A"); + xSemaphoreGive(xSensorDataMutex); + } + + vTaskDelay(pdMS_TO_TICKS(5000)); // 每5秒读取一次 + } +} + +void i2c0_bh1750_task(void *pvParameters) +{ + bh1750_handle_t dev_handle = NULL; + esp_err_t ret = ESP_OK; + + // 创建BH1750传感器对象 + ret = bh1750_create(bus_handle, BH1750_I2C_ADDRESS_DEFAULT, &dev_handle); + if (ret != ESP_OK) + { + ESP_LOGE(BH1750_TAG, "BH1750创建失败,错误码: 0x%x,任务将继续运行但传感器数据无效", ret); + // 不返回,而是进入无限循环,保持传感器无效状态 + while (1) + { + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + g_sensor_data.bh1750_valid = false; + xSemaphoreGive(xSensorDataMutex); + } + vTaskDelay(pdMS_TO_TICKS(5000)); // 每5秒重试一次 + } + } + + // 初始化BH1750 + ret = bh1750_power_on(dev_handle); + if (ret != ESP_OK) + { + ESP_LOGE(BH1750_TAG, "BH1750上电失败,错误码: 0x%x,任务将继续运行但传感器数据无效", ret); + // 删除设备句柄 + bh1750_delete(dev_handle); + // 不返回,而是进入无限循环,保持传感器无效状态 + while (1) + { + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + g_sensor_data.bh1750_valid = false; + xSemaphoreGive(xSensorDataMutex); + } + vTaskDelay(pdMS_TO_TICKS(5000)); // 每5秒重试一次 + } + return; + } + + // 设置测量模式 + ret = bh1750_set_measure_mode(dev_handle, BH1750_CONTINUE_1LX_RES); + if (ret != ESP_OK) + { + ESP_LOGE(BH1750_TAG, "BH1750设置测量模式失败,错误码: 0x%x,任务将继续运行但传感器数据无效", ret); + // 删除设备句柄 + bh1750_delete(dev_handle); + // 不返回,而是进入无限循环,保持传感器无效状态 + while (1) + { + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + g_sensor_data.bh1750_valid = false; + xSemaphoreGive(xSensorDataMutex); + } + vTaskDelay(pdMS_TO_TICKS(5000)); // 每5秒重试一次 + } + return; + } + + // 循环读取数据 + float lux; + while (1) + { + // 读取光照强度 + ret = bh1750_get_data(dev_handle, &lux); + if (ret != ESP_OK) + { + ESP_LOGE(BH1750_TAG, "读取光照强度失败,错误码: 0x%x", ret); + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + g_sensor_data.bh1750_valid = false; + xSemaphoreGive(xSensorDataMutex); + } + } + else + { + // 更新全局传感器数据 + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + g_sensor_data.lux = lux; + g_sensor_data.bh1750_valid = true; + xSemaphoreGive(xSensorDataMutex); + } + + // ESP_LOGI(BH1750_TAG, "光照强度: %.2f lx", lux); + } + + // 更新UI显示 - 需要获取其他传感器数据以更新完整显示 + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + ui_update_sensor_data( + g_sensor_data.ahtxx_valid ? g_sensor_data.temperature : -1.0f, + g_sensor_data.ahtxx_valid ? g_sensor_data.humidity : -1.0f, + g_sensor_data.bh1750_valid ? g_sensor_data.lux : -1.0f, + g_sensor_data.mq135_valid ? g_sensor_data.air_quality : -1.0f, + g_sensor_data.mq135_valid ? (g_sensor_data.air_quality <= 20 ? "Excellent" : g_sensor_data.air_quality <= 100 ? "Good" + : g_sensor_data.air_quality <= 300 ? "Moderate" + : "High") + : "N/A"); + xSemaphoreGive(xSensorDataMutex); + } + + vTaskDelay(pdMS_TO_TICKS(5000)); // 每5秒读取一次 + } +} + +// 获取传感器数据 +void get_sensor_data(sensor_data_t *data) +{ + if (data != NULL) + { + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + *data = g_sensor_data; + xSemaphoreGive(xSensorDataMutex); + } + } +} + +// 打印传感器数据 +void print_sensor_data(void) +{ + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + ESP_LOGI(TAG, "======= Sensor Data ========"); + if (g_sensor_data.ahtxx_valid) + { + ESP_LOGI(TAG, "Temperature: %.2f°C", g_sensor_data.temperature); + ESP_LOGI(TAG, "Humidity: %.2f%%", g_sensor_data.humidity); + } + else + { + ESP_LOGI(TAG, "Temperature: Invalid"); + ESP_LOGI(TAG, "Humidity: Invalid"); + } + + if (g_sensor_data.bh1750_valid) + { + ESP_LOGI(TAG, "Light Intensity: %.2flx", g_sensor_data.lux); + } + else + { + ESP_LOGI(TAG, "Light Intensity: Invalid"); + } + + if (g_sensor_data.mq135_valid) + { + ESP_LOGI(TAG, "Air Quality: %.2f Index", g_sensor_data.air_quality); + } + else + { + ESP_LOGI(TAG, "Air Quality: Invalid"); + } + ESP_LOGI(TAG, "=========================="); + xSemaphoreGive(xSensorDataMutex); + } +} + +// 单独控制舵机的函数 +void servo_set_angle(uint16_t angle) +{ + // 限制角度范围 + if (angle < calibration_value_0) + { + angle = calibration_value_0; + } + else if (angle > calibration_value_180) + { + angle = calibration_value_180; + } + + // 设置舵机角度 + iot_servo_write_angle(LEDC_LOW_SPEED_MODE, 0, angle); + ESP_LOGI(SERVO_TAG, "舵机角度设置为: %d", angle); +} + +static void rx_task(void *arg) +{ + static const char *RX_TASK_TAG = "RX_TASK"; + esp_log_level_set(RX_TASK_TAG, ESP_LOG_INFO); + uint8_t *data = (uint8_t *)malloc(1024 + 1); + static uint8_t frame_buffer[256]; // 帧缓冲区 + static int frame_index = 0; // 帧索引 + + while (1) + { + const int rxBytes = uart_read_bytes(UART_NUM_1, data, 1024, 1000 / portTICK_PERIOD_MS); + if (rxBytes > 0) + { + // 处理接收到的数据 + for (int i = 0; i < rxBytes; i++) + { + // 查找帧头 AA + if (frame_index == 0 && data[i] == 0xAA) + { + frame_buffer[frame_index++] = data[i]; + } + // 检查数据部分是否在01到06范围内 + else if (frame_index == 1 && data[i] >= 0x01 && data[i] <= 0x06) + { + frame_buffer[frame_index++] = data[i]; + } + // 检查帧尾 55 + else if (frame_index == 2 && data[i] == 0x55) + { + frame_buffer[frame_index++] = data[i]; + + // 接收到完整帧 AA [01-06] 55 + ESP_LOGI(RX_TASK_TAG, "Valid frame received: AA 0x%02X 55", frame_buffer[1]); + + // 解析按键状态 + uint8_t data_value = frame_buffer[1]; + + // 根据数据值解析按键状态 + switch (data_value) + { + case 0x01: + ESP_LOGI(RX_TASK_TAG, "Key 1 pressed - LED toggle"); + // 按键1:切换LED灯开关 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + light_source_control_flag = !light_source_control_flag; + led_brightness_value = light_source_control_flag ? 100 : 0; + xSemaphoreGive(xControlFlagMutex); + } + ESP_LOGI(RX_TASK_TAG, "LED light: %s (brightness=%d)", + light_source_control_flag ? "ON" : "OFF", led_brightness_value); + update_telemetry_and_report(); + break; + case 0x02: + ESP_LOGI(RX_TASK_TAG, "Key 2 pressed - Fan toggle"); + // 按键2:切换风扇开关 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + fan_control_flag = !fan_control_flag; + xSemaphoreGive(xControlFlagMutex); + } + ESP_LOGI(RX_TASK_TAG, "Fan: %s", fan_control_flag ? "ON" : "OFF"); + update_telemetry_and_report(); + break; + case 0x03: + ESP_LOGI(RX_TASK_TAG, "Key 3 pressed - Curtain toggle"); + // 按键3:切换窗帘开关 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + servo_control_flag = !servo_control_flag; + xSemaphoreGive(xControlFlagMutex); + } + ESP_LOGI(RX_TASK_TAG, "Curtain: %s", servo_control_flag ? "OPEN" : "CLOSE"); + update_telemetry_and_report(); + break; + case 0x04: + ESP_LOGI(RX_TASK_TAG, "Key 4 pressed - Buzzer toggle"); + // 按键4:切换蜂鸣器开关 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + buzzer_control_flag = !buzzer_control_flag; + xSemaphoreGive(xControlFlagMutex); + } + ESP_LOGI(RX_TASK_TAG, "Buzzer: %s", buzzer_control_flag ? "ON" : "OFF"); + update_telemetry_and_report(); + break; + case 0x05: + ESP_LOGI(RX_TASK_TAG, "Key 5 pressed - Backlight toggle"); + // 按键5:切换屏幕背光开关 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + led_backlight_on = !led_backlight_on; + xSemaphoreGive(xControlFlagMutex); + } + ESP_LOGI(RX_TASK_TAG, "LCD Backlight: %s", led_backlight_on ? "ON" : "OFF"); + update_telemetry_and_report(); + break; + case 0x06: + ESP_LOGI(RX_TASK_TAG, "Key 6 pressed"); + // 切换显示页面(传感器 <-> 时间) + ui_toggle_page(); + break; + default: + ESP_LOGI(RX_TASK_TAG, "Unknown key value: 0x%02X", data_value); + break; + } + + // 重置帧索引,准备接收下一帧 + frame_index = 0; + } + else + { + // 如果不符合协议格式,重置帧索引 + frame_index = 0; + // 检查当前字节是否为帧头 + if (data[i] == 0xAA) + { + frame_buffer[frame_index++] = data[i]; + } + } + } + } + } + free(data); +} + +void initialize_nvs() +{ + esp_err_t ret = nvs_flash_init(); // 初始化NVS, 并检查是否需要擦除NVS + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); +} + +void init_gpio_output() +{ + gpio_config_t io_conf = { + .pin_bit_mask = 1ULL << GPIO_NUM_1, // 配置GPIO1为输出 + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = 0, + .pull_down_en = 1, + .intr_type = GPIO_INTR_DISABLE}; + gpio_config(&io_conf); +} + +/** + * @brief 外设控制任务 + * @param pvParameters 任务参数 + */ +void peripheral_control_task(void *pvParameters) +{ + // 任务初始化代码 + + for (;;) + { + // 周期性更新时间页面(如果在显示时间页面) + ui_time_update(); + + // 在这里实现外设控制逻辑 + // 判断背光状态,并控制背光 + static bool last_led_backlight_on = false; // 记录上一次背光状态 + static bool backlight_first_run = true; // 标记背光控制是否首次运行 + + // 对于首次运行,将last_led_backlight_on设置为与当前值相反,确保首次执行 + if (backlight_first_run) + { + last_led_backlight_on = !led_backlight_on; + backlight_first_run = false; + } + + if (led_backlight_on != last_led_backlight_on) + { // 只在状态变化时执行 + if (led_backlight_on == false) + { + gpio_set_level(EXAMPLE_LCD_GPIO_BL, 0); + } + else + { + gpio_set_level(EXAMPLE_LCD_GPIO_BL, 1); + } + last_led_backlight_on = led_backlight_on; // 更新上次状态 + } + + // 判断窗帘屏状态 + static bool last_servo_control_flag = false; // 初始化为默认值 + static bool first_run = true; // 标记是否首次运行 + + // 对于首次运行,将last_servo_control_flag设置为与当前值相反,确保首次执行 + if (first_run) + { + last_servo_control_flag = !servo_control_flag; + first_run = false; + } + + if (servo_control_flag != last_servo_control_flag) + { // 只在状态变化时执行 + if (servo_control_flag == false) + { + servo_set_angle(0); // 关闭窗帘 + } + else // servo_control_flag 为 true + { + servo_set_angle(180); // 打开窗帘 + } + // 更新遥测数据并上报 + update_telemetry_and_report(); + + last_servo_control_flag = servo_control_flag; // 更新上次状态 + } + + // 风扇的控制逻辑 + static bool last_fan_control_flag = false; // 记录上一次风扇状态 + static bool fan_first_run = true; // 标记风扇控制是否首次运行 + + // 对于首次运行,将last_fan_control_flag设置为与当前值相反,确保首次执行 + if (fan_first_run) + { + last_fan_control_flag = !fan_control_flag; + fan_first_run = false; + } + + if (fan_control_flag != last_fan_control_flag) + { // 只在状态变化时执行 + if (fan_control_flag == false) + { + gpio_set_level(GPIO_NUM_1, 0); // 关闭风扇 + } + else + { + gpio_set_level(GPIO_NUM_1, 1); // 打开风扇 + } + // 更新遥测数据并上报 + update_telemetry_and_report(); + + last_fan_control_flag = fan_control_flag; // 更新上次状态 + } + + // 蜂鸣器控制 + static bool last_buzzer_control_flag = false; // 记录上一次蜂鸣器状态 + static bool buzzer_first_run = true; // 标记蜂鸣器控制是否首次运行 + + // 对于首次运行,将last_buzzer_control_flag设置为与当前值相反,确保首次执行 + if (buzzer_first_run) + { + last_buzzer_control_flag = !buzzer_control_flag; + buzzer_first_run = false; + } + + if (buzzer_control_flag != last_buzzer_control_flag) + { // 只在状态变化时执行 + if (buzzer_control_flag == false) + { + sendControlFrame(0x02, 0); // 关闭蜂鸣器 + } + else + { + sendControlFrame(0x02, 1); // 打开蜂鸣器 + } + // 更新遥测数据并上报 + update_telemetry_and_report(); + + last_buzzer_control_flag = buzzer_control_flag; // 更新上次状态 + } + + // LED灯控制 + static uint8_t last_led_brightness_value = 0; // 记录上一次LED灯亮度 + static bool led_first_run = true; // 标记LED灯控制是否首次运行 + + // 对于首次运行,设置一个不同的初始值以确保首次执行 + if (led_first_run) + { + last_led_brightness_value = !led_brightness_value; // 设置为相反值以确保首次执行 + led_first_run = false; + } + + if (led_brightness_value != last_led_brightness_value) + { // 只在亮度变化时执行 + sendControlFrame(0x01, led_brightness_value); // 设置LED灯亮度 + ESP_LOGI(TAG, "LED brightness updated to %d", led_brightness_value); + // 更新遥测数据并上报 + update_telemetry_and_report(); + + last_led_brightness_value = led_brightness_value; // 更新上次亮度值 + } + + vTaskDelay(pdMS_TO_TICKS(100)); // 默认延时,可根据实际需要调整 + } +} + +static void alarm_clock_main_task(void *arg) +{ + ESP_LOGI(Clock, "启动闹钟系统"); + + initialize_sntp(); // 初始化 SNTP + // 检查时间是否已同步 + // 从 NVS 加载所有闹钟配置 + for (int i = 0; i < ALARM_MAX_NUM; i++) + { + alarm_load_from_nvs(i); + } + + // 3. 初始化闹钟定时器(每秒检查时间) + if (alarm_timer_init() == ESP_OK) + { + ESP_LOGI(Clock, "Alarm timer init success"); + } + + while (1) + { + + // 每秒执行一次 + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + +void app_main(void) +{ + esp_log_level_set("*", ESP_LOG_INFO); + esp_log_level_set("mqtt_client", ESP_LOG_VERBOSE); + esp_log_level_set("mqtt_example", ESP_LOG_VERBOSE); + esp_log_level_set("transport_base", ESP_LOG_VERBOSE); + esp_log_level_set("esp-tls", ESP_LOG_VERBOSE); + esp_log_level_set("transport", ESP_LOG_VERBOSE); + esp_log_level_set("outbox", ESP_LOG_VERBOSE); + esp_log_level_set("dhcps", ESP_LOG_DEBUG); + esp_log_level_set("esp_netif", ESP_LOG_DEBUG); + esp_log_level_set("esp_netif_lwip", ESP_LOG_DEBUG); + esp_log_level_set("wifi", ESP_LOG_DEBUG); + initialize_nvs(); // 初始化NVS + + ESP_ERROR_CHECK(esp_netif_init()); // Initialize ESP-NETIF + // 创建默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // 连接WIFI + ESP_ERROR_CHECK(example_connect()); + + // Print out Access Point Information + wifi_ap_record_t ap_info; + ESP_ERROR_CHECK(esp_wifi_sta_get_ap_info(&ap_info)); + ESP_LOGI(TAG, "--- Access Point Information ---"); + ESP_LOG_BUFFER_HEX("MAC Address", ap_info.bssid, sizeof(ap_info.bssid)); + ESP_LOG_BUFFER_CHAR("SSID", ap_info.ssid, sizeof(ap_info.ssid)); + ESP_LOGI(TAG, "Primary Channel: %d", ap_info.primary); + ESP_LOGI(TAG, "RSSI: %d", ap_info.rssi); + + // 初始化I2C总线 + ESP_ERROR_CHECK(i2c_master_init()); + + // 创建互斥锁 + xSensorDataMutex = xSemaphoreCreateMutex(); + if (xSensorDataMutex == NULL) + { + ESP_LOGE(TAG, "创建传感器数据互斥锁失败"); + } + + // 添加MQTT消息互斥锁 + xMqttMessageMutex = xSemaphoreCreateMutex(); + if (xMqttMessageMutex == NULL) + { + ESP_LOGE(TAG, "创建MQTT消息互斥锁失败"); + } + + // 创建时间段配置互斥锁 + xTimePeriodMutex = xSemaphoreCreateMutex(); + if (xTimePeriodMutex == NULL) + { + ESP_LOGE(TAG, "创建时间段配置互斥锁失败"); + } + + // 创建控制标志互斥锁 + xControlFlagMutex = xSemaphoreCreateMutex(); + if (xControlFlagMutex == NULL) + { + ESP_LOGE(TAG, "创建控制标志互斥锁失败"); + } + + // 初始化舵机 + servo_init(); + + // GPIO输出初始化(风扇控制) + init_gpio_output(); + + // 初始化LVGL界面 + start_lvgl_demo(); + + // MCU间的串口通信初始化 + serial_mcu_init(); + + mqtt_app_start(); // 启动 MQTT 客户端 + xTaskCreate(alarm_clock_main_task, "alarm_clock", 8192, NULL, 5, NULL); + // 创建时间段检查任务 + xTaskCreate(time_period_check_task, "time_period_task", 4096, NULL, 5, NULL); + // 创建降温模式任务 + xTaskCreate(cooling_mode_task, "cooling_mode_task", 4096, NULL, 5, NULL); + // 创建自动通风控制模式任务 + xTaskCreate(ventilation_mode_task, "ventilation_mode_task", 4096, NULL, 5, NULL); + // 创建MQ135传感器任务 + xTaskCreate(mq135_task, "mq135_task", 4096, NULL, 5, NULL); + // 创建外设控制任务 + xTaskCreate(peripheral_control_task, "peripheral_control_task", 4096, NULL, 5, NULL); + + // 创建任务 + xTaskCreate(i2c0_ahtxx_task, "i2c0_ahtxx_task", 4096, NULL, 5, NULL); + xTaskCreate(i2c0_bh1750_task, "i2c0_bh1750_task", 4096, NULL, 5, NULL); + // 创建接收任务 + xTaskCreate(rx_task, "uart_rx_task", 4096, NULL, configMAX_PRIORITIES - 1, NULL); + + while (1) + { + // 定期打印传感器数据 + // print_sensor_data(); + vTaskDelay(5000 / portTICK_PERIOD_MS); + } +} + +/** + * @brief 降温模式任务 + * 监测温度,当温度超过阈值时自动开启风扇 + * 当温度超过35°C时触发高温提醒 + */ +static void cooling_mode_task(void *pvParameters) +{ + ESP_LOGI(COOLING_MODE_TAG, "Cooling mode task started"); + + // 从NVS加载配置 + cooling_mode_load_from_nvs(); + + // 高温阈值 + const float HIGH_TEMP_THRESHOLD = 35.0f; + + while (1) + { + if (g_cooling_mode_enabled) + { + float current_temp = 0; + bool temp_valid = false; + + // 获取当前温度 + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + current_temp = g_sensor_data.temperature; + temp_valid = g_sensor_data.ahtxx_valid; + xSemaphoreGive(xSensorDataMutex); + } + + if (temp_valid) + { + // 降温模式:温度超过阈值,开启风扇 + if (current_temp > g_temperature_threshold) + { + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + if (!fan_control_flag) + { + fan_control_flag = true; + ESP_LOGI(COOLING_MODE_TAG, "Temperature %.1f°C > %.1f°C, cooling mode: Fan ON", + current_temp, g_temperature_threshold); + update_telemetry_and_report(); + } + xSemaphoreGive(xControlFlagMutex); + } + } + // 温度恢复到阈值以下,关闭风扇 + else if (current_temp < (g_temperature_threshold - 1.0f)) // 添加1°C的滞后,防止频繁切换 + { + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + if (fan_control_flag) + { + fan_control_flag = false; + ESP_LOGI(COOLING_MODE_TAG, "Temperature %.1f°C < %.1f°C, cooling mode: Fan OFF", + current_temp, g_temperature_threshold - 1.0f); + update_telemetry_and_report(); + } + xSemaphoreGive(xControlFlagMutex); + } + } + + // 高温提醒:温度超过35°C + if (current_temp > HIGH_TEMP_THRESHOLD) + { + if (!g_high_temp_alerted) + { + g_high_temp_alerted = true; + + // 蜂鸣器发出高温提示音 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + buzzer_control_flag = true; + xSemaphoreGive(xControlFlagMutex); + } + + ESP_LOGW(COOLING_MODE_TAG, "High temperature alert: %.1f°C (>35°C)", current_temp); + + // 发送MQTT提醒消息 + if (g_mqtt_client != NULL) + { + cJSON *root = cJSON_CreateObject(); + cJSON_AddStringToObject(root, "type", "device_message"); + cJSON_AddStringToObject(root, "device_id", g_device_message.device_id); + cJSON_AddStringToObject(root, "device_type", g_device_message.device_type); + cJSON_AddStringToObject(root, "message_type", "high_temp_alert"); + + cJSON *data_obj = cJSON_CreateObject(); + cJSON_AddStringToObject(data_obj, "alert", "温度过高请注意通风"); + cJSON_AddNumberToObject(data_obj, "temperature", roundf(current_temp * 10) / 10); + cJSON_AddItemToObject(root, "data", data_obj); + + char *json_message = cJSON_Print(root); + if (json_message) + { + esp_mqtt_client_publish(g_mqtt_client, MQTT_PUBLISH_TOPIC_QOS0, json_message, 0, 0, 0); + ESP_LOGI(COOLING_MODE_TAG, "High temp alert sent: %s", json_message); + free(json_message); + } + cJSON_Delete(root); + } + update_telemetry_and_report(); + } + } + else + { + // 温度恢复正常,重置高温提醒标志 + if (g_high_temp_alerted) + { + g_high_temp_alerted = false; + // 关闭高温提醒蜂鸣器 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + buzzer_control_flag = false; + xSemaphoreGive(xControlFlagMutex); + } + ESP_LOGI(COOLING_MODE_TAG, "Temperature normalized: %.1f°C, reset high temp alert", current_temp); + update_telemetry_and_report(); + } + } + } + } + + // 每5秒检查一次 + vTaskDelay(pdMS_TO_TICKS(5000)); + } +} + +/** + * @brief 自动通风控制模式任务 + * 监测空气质量,当空气质量大于50时自动开启风扇并发送提醒 + */ +static void ventilation_mode_task(void *pvParameters) +{ + ESP_LOGI(VENTILATION_MODE_TAG, "Ventilation mode task started"); + + while (1) + { + if (g_ventilation_mode_enabled) + { + float current_air_quality = 0; + bool air_quality_valid = false; + + // 获取当前空气质量 + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + current_air_quality = g_sensor_data.air_quality; + air_quality_valid = g_sensor_data.mq135_valid; + xSemaphoreGive(xSensorDataMutex); + } + + if (air_quality_valid) + { + // 检测空气质量是否超过阈值 + if (current_air_quality > AIR_QUALITY_THRESHOLD) + { + // 需要通风,确保风扇开启 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + if (!fan_control_flag) + { + fan_control_flag = true; + ESP_LOGW(VENTILATION_MODE_TAG, "Air quality %.2f > %.1f, Fan ON", + current_air_quality, AIR_QUALITY_THRESHOLD); + update_telemetry_and_report(); + } + xSemaphoreGive(xControlFlagMutex); + } + + // 发送提醒(只发送一次) + if (!g_air_quality_alerted) + { + g_air_quality_alerted = true; + + // 蜂鸣器发出轻微提示音(短促) + sendControlFrame(0x02, 1); + vTaskDelay(pdMS_TO_TICKS(200)); // 持续200ms + sendControlFrame(0x02, 0); + + // 发送MQTT提醒消息 + if (g_mqtt_client != NULL) + { + cJSON *root = cJSON_CreateObject(); + cJSON_AddStringToObject(root, "type", "device_message"); + cJSON_AddStringToObject(root, "device_id", g_device_message.device_id); + cJSON_AddStringToObject(root, "device_type", g_device_message.device_type); + cJSON_AddStringToObject(root, "message_type", "air_quality_alert"); + + cJSON *data_obj = cJSON_CreateObject(); + cJSON_AddStringToObject(data_obj, "alert", "卧室需要通风"); + cJSON_AddNumberToObject(data_obj, "air_quality", roundf(current_air_quality * 10) / 10); + cJSON_AddItemToObject(root, "data", data_obj); + + char *json_message = cJSON_Print(root); + if (json_message) + { + esp_mqtt_client_publish(g_mqtt_client, MQTT_PUBLISH_TOPIC_QOS0, json_message, 0, 0, 0); + ESP_LOGI(VENTILATION_MODE_TAG, "Air quality alert sent: %s", json_message); + free(json_message); + } + cJSON_Delete(root); + } + } + } + else if (current_air_quality < (AIR_QUALITY_THRESHOLD - 10)) // 添加滞后,防止频繁切换 + { + // 空气质量恢复良好,关闭风扇 + if (xControlFlagMutex != NULL && xSemaphoreTake(xControlFlagMutex, portMAX_DELAY) == pdTRUE) + { + if (fan_control_flag) + { + fan_control_flag = false; + ESP_LOGI(VENTILATION_MODE_TAG, "Air quality %.2f < %.1f, Fan OFF", + current_air_quality, AIR_QUALITY_THRESHOLD - 10); + update_telemetry_and_report(); + } + xSemaphoreGive(xControlFlagMutex); + } + + // 重置提醒标志 + if (g_air_quality_alerted) + { + g_air_quality_alerted = false; + ESP_LOGI(VENTILATION_MODE_TAG, "Air quality normalized: %.2f, reset alert", current_air_quality); + } + } + } + } + + // 每5秒检查一次 + vTaskDelay(pdMS_TO_TICKS(5000)); + } +} + +// MQ135传感器任务 +void mq135_task(void *pvParameters) +{ + // 初始化ADC + adc1_config_width(ADC_WIDTH_BIT_12); // ADC1 分辨率 12 位 + adc1_config_channel_atten(ADC_CHANNEL_0, ADC_ATTEN_DB_12); // GPIO34, 12dB + + // 动态校准变量 + static bool r0_calibrated = false; + static float R0 = 10.0f; // 初始 R0(更合理的经验值,10kΩ左右) + static int calibrate_count = 0; // 校准采样计数器 + static float rs_sum = 0.0f; // Rs累加值 + + // 采样平滑滤波窗口 + #define SAMPLE_WINDOW_SIZE 20 // 滑动平均窗口大小 + static float voltage_buffer[SAMPLE_WINDOW_SIZE] = {0}; + static int buffer_index = 0; + static bool buffer_filled = false; + + // 预热阶段 + #define WARMUP_SAMPLES 100 // 预热采样次数 + static int warmup_count = 0; + + ESP_LOGI(MQ135_TAG, "MQ135 sensor task started - Preheating for %d samples...", WARMUP_SAMPLES); + + while (1) + { + int adc_value = adc1_get_raw(ADC_CHANNEL_0); + + // ADC -> 电压(工程近似) + float voltage = (adc_value * 3.3f) / 4095.0f; + + // 电压保护 + if (voltage >= 3.3f) + voltage = 3.29f; + if (voltage <= 0.0f) + voltage = 0.01f; + + // === 采样平滑滤波 === + // 存入滑动窗口 + voltage_buffer[buffer_index] = voltage; + buffer_index = (buffer_index + 1) % SAMPLE_WINDOW_SIZE; + + // 检查窗口是否填满 + if (buffer_index == 0) + { + buffer_filled = true; + } + + // 计算滑动平均电压 + float smoothed_voltage = 0.0f; + int window_size = buffer_filled ? SAMPLE_WINDOW_SIZE : buffer_index; + + for (int i = 0; i < window_size; i++) + { + smoothed_voltage += voltage_buffer[i]; + } + smoothed_voltage /= window_size; + + // 计算 Rs(RL = 10kΩ) + float rs = ((3.3f - smoothed_voltage) / smoothed_voltage) * 10.0f; + + // === 预热阶段 === + if (warmup_count < WARMUP_SAMPLES) + { + warmup_count++; + if (warmup_count % 20 == 0) // 每20个采样打印一次进度 + { + ESP_LOGI(MQ135_TAG, "MQ135 preheating... %d/%d", warmup_count, WARMUP_SAMPLES); + } + vTaskDelay(5000 / portTICK_PERIOD_MS); + continue; + } + else if (warmup_count == WARMUP_SAMPLES) + { + warmup_count++; + ESP_LOGI(MQ135_TAG, "MQ135 preheating completed, starting calibration"); + vTaskDelay(100 / portTICK_PERIOD_MS); // 短暂延迟 + continue; + } + + // === 温湿度补偿 === + // MQ135传感器对温度和湿度敏感,需要进行补偿 + float temperature = 20.0f; // 默认20°C + float humidity = 65.0f; // 默认65%RH + + // 获取实际温湿度 + if (xSensorDataMutex != NULL && xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + if (g_sensor_data.ahtxx_valid) + { + temperature = g_sensor_data.temperature; + humidity = g_sensor_data.humidity; + } + xSemaphoreGive(xSensorDataMutex); + } + + // 温度补偿公式 (参考MQ135数据手册) + // R0(T) = R0(T0) * exp(A * (1/T - 1/T0)) + // A为温度系数,约为0.00025 + float temp_compensation = 1.0f + 0.00025f * (temperature - 20.0f); + + // 湿度补偿 (经验公式) + float hum_compensation = 1.0f + 0.0005f * (humidity - 65.0f); + + // 应用补偿后的Rs + float rs_compensated = rs / (temp_compensation * hum_compensation); + + // === R0校准阶段 === + if (!r0_calibrated && calibrate_count < 100) + { + rs_sum += rs_compensated; + calibrate_count++; + + if (calibrate_count == 100) + { + // 使用100次采样的平均值作为R0(假设在干净空气中启动) + R0 = rs_sum / 100.0f; + ESP_LOGI(MQ135_TAG, "MQ135 R0 calibrated to: %.2f (from 100 samples, temp=%.1f°C, hum=%.1f%%)", + R0, temperature, humidity); + r0_calibrated = true; + } + else if (calibrate_count % 20 == 0) + { + ESP_LOGI(MQ135_TAG, "Calibrating R0... %d/100", calibrate_count); + } + } + else if (!r0_calibrated) + { + // 如果校准未完成,使用初始R0值 + ESP_LOGI(MQ135_TAG, "Using initial R0: %.2f", R0); + } + + // Rs/R0 比值 + float ratio = rs_compensated / R0; + + // 防止ratio异常(传感器故障或校准错误) + if (ratio <= 0.1f || ratio > 100.0f) + { + ESP_LOGW(MQ135_TAG, "Abnormal ratio detected: %.3f, skipping", ratio); + vTaskDelay(5000 / portTICK_PERIOD_MS); + continue; + } + + /* ===================================================== + * MQ135 传感器方程 + * Rs/R0 = a * (ppm)^b + * ppm = (Rs/R0 / a)^(1/b) + * 对于 MQ135: a=116.602, b=-2.769 + * ===================================================== */ + float mq135_concentration = 116.602f * powf(ratio, -2.769f); + + // 工程修正:不使用放大系数 + mq135_concentration = mq135_concentration * 1.0f; + + // 下限保护,避免负值或过小值 + if (mq135_concentration < 1.0f) + mq135_concentration = 1.0f; + + // 上限保护 + if (mq135_concentration > 1000.0f) + { + ESP_LOGW(MQ135_TAG, "Concentration too high: %.2f, clamped to 1000", mq135_concentration); + mq135_concentration = 1000.0f; + } + + // 保留两位小数 + mq135_concentration = roundf(mq135_concentration * 100.0f) / 100.0f; + + // 更新全局数据 + if (xSensorDataMutex != NULL && + xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + g_sensor_data.air_quality = mq135_concentration; + g_sensor_data.mq135_valid = true; + xSemaphoreGive(xSensorDataMutex); + } + + // UI 更新 + if (xSensorDataMutex != NULL && + xSemaphoreTake(xSensorDataMutex, portMAX_DELAY) == pdTRUE) + { + ui_update_sensor_data( + g_sensor_data.ahtxx_valid ? g_sensor_data.temperature : -1.0f, + g_sensor_data.ahtxx_valid ? g_sensor_data.humidity : -1.0f, + g_sensor_data.bh1750_valid ? g_sensor_data.lux : -1.0f, + mq135_concentration, + mq135_concentration <= 20 ? "Excellent" : mq135_concentration <= 100 ? "Good" + : mq135_concentration <= 300 ? "Moderate" + : "High"); + xSemaphoreGive(xSensorDataMutex); + } + + // 每10次采样打印一次详细日志 + static int log_count = 0; + log_count++; + if (log_count >= 10) + { + log_count = 0; + ESP_LOGI(MQ135_TAG, "ADC:%d, Volt:%.3fV->%.3fV, Rs:%.2f, Ratio:%.3f, PPM:%.2f (T:%.1f°C, H:%.1f%%)", + adc_value, voltage, smoothed_voltage, rs, ratio, mq135_concentration, temperature, humidity); + } + + vTaskDelay(5000 / portTICK_PERIOD_MS); + } +} diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..bd1a774 --- /dev/null +++ b/partitions.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 0x200000, +wifi_spiffs, data, spiffs, 0x210000, 0x180000, \ No newline at end of file