Commit f81a86c8 authored by 957dd's avatar 957dd

Merge branch 'feature/defense_file' into 'master'

进行了分文件何加入一些逻辑(比如烟雾) See merge request !1
parents 51d9c0ac 509bf8cd
#include "AudioManager.h"
#include "PubSubClient.h" // MQTTManager 内部使用
// 构造函数实现
AudioManager::AudioManager(int bckPin, int lrcPin, int dinPin, int sampleRate, int dmaBufCount, int dmaBufLen)
: _bckPin(bckPin), _lrcPin(lrcPin), _dinPin(dinPin),
_sampleRate(sampleRate), _dmaBufCount(dmaBufCount), _dmaBufLen(dmaBufLen) {
// 构造函数体
}
// 初始化 I2S 实现
bool AudioManager::begin() {
Serial.println("Initializing I2S...");
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), // 设置为主机模式,发送数据
.sample_rate = _sampleRate, // 设置采样率
.bits_per_sample = (i2s_bits_per_sample_t)_bitsPerSample, // 设置每个采样点的位数
.channel_format = _channelFormat, // 通道格式
.communication_format = _commFormat, // 通信格式
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // 中断分配标志
.dma_buf_count = _dmaBufCount, // DMA缓冲区数量
.dma_buf_len = _dmaBufLen, // 每个DMA缓冲区的长度
.use_apll = false, // 不使用APLL
.tx_desc_auto_clear = true, // 自动清除已发送的TX描述符
.fixed_mclk = 0 // 不使用固定的MCLK
};
// 安装I2S驱动
esp_err_t err = i2s_driver_install(_i2sPort, &i2s_config, 0, NULL);
if (err != ESP_OK) {
Serial.printf("I2S driver install failed: %d\n", err);
return false;
}
Serial.println("I2S driver installed.");
// 配置I2S引脚
i2s_pin_config_t pin_config = {
.bck_io_num = _bckPin, // 位时钟引脚
.ws_io_num = _lrcPin, // 左右时钟/字选择引脚
.data_out_num = _dinPin, // 数据输出引脚
.data_in_num = I2S_PIN_NO_CHANGE // 不使用数据输入引脚
};
err = i2s_set_pin(_i2sPort, &pin_config);
if (err != ESP_OK) {
Serial.printf("I2S pin setting failed: %d\n", err);
return false;
}
Serial.println("I2S pins set.");
return true;
}
// 请求播放实现
void AudioManager::requestPlay() {
_playRequested = true;
}
// 检查是否正在播放(简单实现)
bool AudioManager::isPlaying() const {
// 更精确的实现需要跟踪播放状态,这里只是基于请求标志
return _playRequested; // 在play()函数结束时需要将此标志设回false
}
// 内部播放函数实现
void AudioManager::play() {
Serial.println("Start playing audio...");
size_t bytes_written = 0;
int current_pos = 0;
// 创建一个RAM缓冲区,用于从PROGMEM拷贝数据块
uint8_t ram_buffer[_dmaBufLen];
// 循环直到所有音频数据都被发送
while (current_pos < pcm_sound_data_len) {
int bytes_to_copy = _dmaBufLen;
if (current_pos + bytes_to_copy > pcm_sound_data_len) {
bytes_to_copy = pcm_sound_data_len - current_pos;
}
// 从PROGMEM拷贝数据到RAM缓冲区
memcpy_P(ram_buffer, &pcm_sound_data[current_pos], bytes_to_copy);
// 将RAM缓冲区中的数据写入I2S总线
esp_err_t result = i2s_write(_i2sPort, ram_buffer, bytes_to_copy, &bytes_written, portMAX_DELAY);
if (result != ESP_OK) {
Serial.printf("I2S write error: %d\n", result);
break;
}
// if (bytes_written < bytes_to_copy) {
// Serial.printf("I2S write incomplete: Wrote %d / %d bytes\n", bytes_written, bytes_to_copy);
// }
current_pos += bytes_written; // 更新当前位置
}
// 播放完毕后,清空DMA缓冲区确保静音
i2s_zero_dma_buffer(_i2sPort);
Serial.println("Audio playback finished.");
_playRequested = false; // 重置播放请求标志
// 播放结束后,如果 MQTTManager 指针有效,发送播放结束消息
// if (_mqttManager) {
// Serial.println("Publishing audio play end message...");
// _mqttManager->publishJson("audioplayend", 1);
// }
}
// FreeRTOS 任务函数实现
// 这个函数运行在一个单独的核心上,负责检查播放请求并调用播放函数
void audioPlayTask(void* pvParameters) {
AudioManager* audioManager = static_cast<AudioManager*>(pvParameters);
if (!audioManager) {
Serial.println("AudioPlay Task: Invalid parameter!");
vTaskDelete(NULL); // 删除当前任务
return;
}
for (;;) {
if (audioManager->_playRequested) {
audioManager->play();
}
vTaskDelay(pdMS_TO_TICKS(50)); // 短暂延时,避免忙等
}
}
\ No newline at end of file
#ifndef AUDIO_MANAGER_H
#define AUDIO_MANAGER_H
#include <Arduino.h>
#include "pcm.h" // 包含你的PCM音频数据头文件
#include "driver/i2s.h" // 包含ESP32 I2S驱动头文件
#include "MQTTManager.h" // 需要与 MQTTManager 交互以发送播放结束消息
class MQTTManager; // 前向声明,避免循环包含
class AudioManager {
private:
// I2S 配置参数
i2s_port_t _i2sPort = I2S_NUM_0; // 使用I2S端口0 (可配置)
int _sampleRate = 16000; // 音频采样率 (Hz) (可配置)
int _bitsPerSample = I2S_BITS_PER_SAMPLE_16BIT; // 每个采样点的位数
i2s_channel_fmt_t _channelFormat = I2S_CHANNEL_FMT_RIGHT_LEFT; // 通道格式
i2s_comm_format_t _commFormat = I2S_COMM_FORMAT_STAND_I2S; // 通信格式
int _dmaBufCount = 8; // DMA缓冲区数量 (可配置)
int _dmaBufLen = 1024; // 每个DMA缓冲区的长度 (字节) (可配置)
// I2S 引脚
int _bckPin;
int _lrcPin;
int _dinPin; // MAX98357A 不需要 DIN
bool _playRequested = false; // 播放请求标志
MQTTManager* _mqttManager = nullptr; // MQTT 管理器指针,用于发送播放结束消息
// 内部播放函数(在任务中调用)
void play();
public:
// 构造函数
AudioManager(int bckPin, int lrcPin, int dinPin, int sampleRate = 16000, int dmaBufCount = 8, int dmaBufLen = 1024);
// 初始化 I2S
bool begin();
// 请求播放音频(任务会检查此标志)
void requestPlay();
// 检查当前是否正在播放(如果play()是阻塞的,这个可能没意义,除非在play()内部设置标志)
bool isPlaying() const;
// 设置 MQTTManager 指针
void setMQTTManager(MQTTManager* mqttManager) { _mqttManager = mqttManager; }
// FreeRTOS 任务函数(需要在主文件或单独的.cpp中实现,并友元声明)
// Friend declaration to allow the task function to access private members if needed
friend void audioPlayTask(void* pvParameters);
};
extern AudioManager audioManager;
// FreeRTOS 任务函数声明
void audioPlayTask(void* pvParameters);
#endif // AUDIO_MANAGER_H
\ No newline at end of file
#include "DeviceControl.h"
#include "AudioManager.h"
#include <ArduinoJson.h> // 需要用于 handleMQTTCommand
// 构造函数实现
DeviceControl::DeviceControl(int pin1, int pin2, int pin3, int pin4, int sensorPin)
: _sensorPin(sensorPin) {
_outputPins[0] = pin1;
_outputPins[1] = pin2;
_outputPins[2] = pin3;
_outputPins[3] = pin4;
}
// begin 方法实现
void DeviceControl::begin() {
Serial.println("DeviceControl initializing...");
// 配置引脚模式
for (int i = 0; i < 4; ++i) {
pinMode(_outputPins[i], OUTPUT);
digitalWrite(_outputPins[i], LOW); // 确保启动时为低电平
}
pinMode(_sensorPin, INPUT); // 传感器引脚设为输入
Serial.println("DeviceControl initialized.");
}
// 设置数字输出实现 (1-4 对应 _outputPins 数组索引 0-3)
void DeviceControl::setPinValue(int pinIndex, int val) {
if (pinIndex >= 1 && pinIndex <= 4) {
//Serial.printf("Setting digital pin %d (%d) to %d\n", pinIndex, _outputPins[pinIndex - 1], val);
digitalWrite(_outputPins[pinIndex - 1], val == 1 ? HIGH : LOW);
} else {
Serial.printf("Warning: Invalid pin index for digital write: %d\n", pinIndex);
}
}
// 设置 PWM 输出实现 (1-4 对应 _outputPins 数组索引 0-3)
void DeviceControl::setPWMValue(int pinIndex, int val) {
if (pinIndex >= 1 && pinIndex <= 4) {
// analogWrite 在 ESP32 上是 8 位分辨率 (0-255)
int analogVal = constrain(val * 255 / 100, 0, 255); // 将输入的 0-100 值映射到 0-255
//Serial.printf("Setting PWM pin %d (%d) to %d (scaled to %d)\n", pinIndex, _outputPins[pinIndex - 1], val, analogVal);
analogWrite(_outputPins[pinIndex - 1], analogVal);
} else {
Serial.printf("Warning: Invalid pin index for PWM write: %d\n", pinIndex);
}
}
// 更新 LED 血量效果实现
void DeviceControl::updateLedBlood() {
// 这个函数需要在周期性的任务中被调用,比如每几十毫秒一次
_led_count++; // 每次调用都递增计数器
if (_led_count > 20) _led_count = 0; // 闪烁周期计数器
if (_ledblood >= 6) { // 满血或更高,3个灯全亮
setPinValue(1,1);
setPinValue(2,1);
setPinValue(3,1);
} else if (_ledblood == 5){ // 5格血,灯1闪烁,灯2、3常亮
setPinValue(1, (_led_count < 10)); // 计数器 < 10 时为高电平
setPinValue(2,1);
setPinValue(3,1);
} else if (_ledblood == 4){ // 4格血,灯1灭,灯2、3常亮
setPinValue(1,0);
setPinValue(2,1);
setPinValue(3,1);
} else if (_ledblood == 3){ // 3格血,灯2闪烁,灯3常亮,灯1灭
setPinValue(2, (_led_count < 10));
setPinValue(1,0);
setPinValue(3,1);
} else if (_ledblood == 2){ // 2格血,灯3常亮,灯1、2灭
setPinValue(1,0);
setPinValue(2,0);
setPinValue(3,1);
} else if (_ledblood == 1){ // 1格血,灯3闪烁,灯1、2灭
setPinValue(3, (_led_count < 10));
setPinValue(1,0);
setPinValue(2,0);
} else { // ledblood <= 0,所有灯灭
setPinValue(1,0);
setPinValue(2,0);
setPinValue(3,0);
}
}
// 读取传感器值实现
double DeviceControl::readSensor() {
int rawValue = analogRead(_sensorPin);
// 将读数转换为电压(假设 ADC 12位,参考电压 3.3V 或 2.5V)
// ESP32C3 ADC 可以是 12位 (0-4095),参考电压通常是 3.3V 内部或外部。
// 原始代码使用了 2.5,我们假设是 2.5V 参考电压,或者原始代码有误。
// 使用 3.3V 参考电压是更常见的做法: rawValue * 3.3 / 4095.0
// 如果你的电路使用了外部 2.5V 参考或其他方式,请调整下面的乘数
const double referenceVoltage = 3.3; // 假设使用 3.3V 参考电压
double voltage = (double)rawValue * referenceVoltage / 4095.0;
// Serial.printf("Sensor raw: %d, Voltage: %.2fV\n", rawValue, voltage); // 调试信息
return voltage;
}
// 处理来自 MQTT 的控制指令
void DeviceControl::handleMQTTCommand(int message_Type, JsonObject body) {
message_type msgType = static_cast<message_type>(message_Type); ;
if (msgType == message_type::PIN_CONTROL || msgType == message_type::PWM_CONTROL) { // GPIO 或 PWM 控制
int pin = 0, val = 0;
if (body.containsKey("pin")) {
pin = body["pin"]; // 获取引脚编号 (1-4)
}
if (body.containsKey("val")) {
val = body["val"]; // 获取值
}
if (msgType == message_type::PIN_CONTROL) { // 数字IO控制
setPinValue(pin, val);
} else if (msgType == message_type::PWM_CONTROL) { // PWM控制
setPWMValue(pin, val);
}
Serial.printf("Processed command: pin=%d, val=%d (Type %d)\n", pin, val, msgType);
} else if (msgType == message_type::SERSOR_CONTROL) { // 动作控制
if (body.containsKey("action")) {
String action = body["action"].as<String>();
int val = 0;
if (body.containsKey("val")) {
val = body["val"]; // 获取值
}
if (action == "led") {
setLedBlood(val); // 设置 LED 血量Connected
Serial.printf("Processed action: led, val=%d\n", val);
} else if (action == "fog_ms") {
// 原代码中有 time_count=0,这里可以根据需要实现烟雾机控制逻辑
Serial.printf("Processed action: fog_ms, val=%d (Not fully implemented)\n", val);
} // audio 动作在 MQTT 回调中处理并请求 AudioManager 播放
else if (action == "audio") {
audioManager.requestPlay(); // 请求音频播放
_mqttManager->publishJson("audioplayend", 102);
// 原代码中有 time_count=0,这里可以根据需要实现烟雾机控制逻辑
Serial.printf("audio playend\n", val);
}else if (action == "smoke_ms") {
_smoke_count=0;
_smoke_ms=val;
_smoke_index=true;
// 原代码中有 time_count=0,这里可以根据需要实现烟雾机控制逻辑
Serial.printf("smoke set\n");
}else {
Serial.printf("Unknown action: %s\n", action.c_str());
}
}
}
}
void DeviceControl::control_smoke() {
int smoke_i = _smoke_ms / 50;
if(_smoke_count < smoke_i){
setPinValue(4,1);
}else setPinValue(4,0);
}
// 更新方法,在周期性任务中调用
void DeviceControl::update() {
// 更新 LED 状态
updateLedBlood();
if(_smoke_index = true){
control_smoke();
//更新烟雾模块
_smoke_count++;
if(_smoke_count>2000) _smoke_count =2001;
}
// 读取传感器并发布数据 (可以设置一个频率,而不是每次 update() 都发布)
// 简单的实现:每隔一定时间或阈值触发时发布
// static unsigned long lastSensorPublishTime = 0;
// const unsigned long sensorPublishInterval = 50; // 每 1 秒发布一次传感器数据
// if (millis() - lastSensorPublishTime >= sensorPublishInterval) {
double sensorVoltage = readSensor();
// 原始代码中的阈值判断
if (sensorVoltage >= 1.25&& mqttManager.wifi_connected_is()==0) { // 传感器值超过阈值
Serial.println("Sensor threshold reached, publishing shoted message.");
if (_mqttManager) {
// 发布 "shoted" 消息
_mqttManager->publishJson("shoted", 101);
} else {
Serial.println("MQTTManager not set, cannot publish sensor data.");
}
}
// lastSensorPublishTime = millis();
// }
}
// FreeRTOS 任务函数实现
void devicePeriodicTask(void* pvParameters) {
DeviceControl* deviceControl = static_cast<DeviceControl*>(pvParameters);
if (!deviceControl) {
Serial.println("Device Periodic Task: Invalid parameter!");
vTaskDelete(NULL);
return;
}
for (;;) {
deviceControl->update(); // 调用 DeviceControl 的 update 方法
vTaskDelay(pdMS_TO_TICKS(50)); // 每 50 毫秒更新一次
}
}
\ No newline at end of file
#ifndef DEVICE_CONTROL_H
#define DEVICE_CONTROL_H
#include <Arduino.h>
#include "MQTTManager.h" // 需要与 MQTTManager 交互以发布传感器数据
class MQTTManager; // 前向声明
class DeviceControl {
private:
int _outputPins[4]; // 存储输出引脚的 GPIO 编号
int _sensorPin; // 存储传感器输入引脚的 GPIO 编号
int _ledblood = 6; // 用于控制 LED "血量" 效果的状态变量
int _led_count = 0; // LED 闪烁效果的计数器 (用于 updateLedBlood)
int _smoke_count = 0;
int _smoke_ms = 0;
bool _smoke_index =false;
MQTTManager* _mqttManager = nullptr; // MQTT 管理器指针,用于发布传感器数据
// 内部函数
void setPinValue(int pinIndex, int val); // 控制数字输出
void setPWMValue(int pinIndex, int val); // 控制 PWM 输出
// 更新 LED 血量效果
void updateLedBlood();
// 读取传感器值
double readSensor();
public:
// 构造函数
// 传入输出引脚和传感器引脚的 GPIO 编号
DeviceControl(int pin1, int pin2, int pin3, int pin4, int sensorPin);
// 初始化引脚模式等
void begin();
// 设置 MQTTManager 指针
void setMQTTManager(MQTTManager* mqttManager) { _mqttManager = mqttManager; }
// 从外部设置 LED 血量值 (例如来自 MQTT)
void setLedBlood(int blood) { _ledblood = constrain(blood, 0, 6); _led_count = 0; } // 限制范围,重置计数器
// 处理来自 MQTT 的控制指令(例如设置引脚或 PWM)
void handleMQTTCommand(int message_type, JsonObject body);
// 在周期性任务中调用,更新LED状态并读取/发布传感器数据
void update();
// FreeRTOS 任务函数(需要在主文件或单独的.cpp中实现,并友元声明)
friend void devicePeriodicTask(void* pvParameters);
//烟雾传感器
void control_smoke();
// 枚举类作为 MqttClient 的成员
enum class message_type : int {
PIN_CONTROL = 3,
PWM_CONTROL = 4,
SERSOR_CONTROL = 5
};
};
// FreeRTOS 任务函数声明
void devicePeriodicTask(void* pvParameters);
#endif // DEVICE_CONTROL_H
\ No newline at end of file
#include "MQTTManager.h"
#include <WiFi.h> // 需要检查 WiFi 连接状态
#include <string> // For std::string
// 初始化静态成员指针
MQTTManager* MQTTManager::_instance = nullptr;
// 构造函数实现
MQTTManager::MQTTManager(const char* broker, int port, const char* username, const char* password,
const char* subscribeTopic, const char* publishTopic)
: _mqttClient(_espClient), // 初始化 PubSubClient
_broker(broker), _port(port), _username(username), _password(password),
_subscribeTopic(subscribeTopic), _publishTopic(publishTopic) {
// 在构造函数中设置静态实例指针
_instance = this;
}
// begin 方法实现
void MQTTManager::begin() {
Serial.println("MQTTManager initializing...");
_mqttClient.setServer(_broker.c_str(), _port);
// 设置 PubSubClient 的回调函数为我们的 wrapper
_mqttClient.setCallback(MQTTManager::pubSubCallbackWrapper);
// 生成 MQTT 客户端ID
uint8_t mac[6];
WiFi.macAddress(mac);
char clientIdChar[20]; // 需要足够的空间
sprintf(clientIdChar, "ESP32-%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
_clientId = clientIdChar;
Serial.print("Generated MQTT Client ID: ");
Serial.println(_clientId.c_str());
}
// 设置消息回调函数
void MQTTManager::setCallback(MqttCallbackType callback) {
_callback = callback;
}
// PubSubClient 回调包装函数实现
// 这是 PubSubClient 调用的函数,它再调用我们存储的用户回调
void MQTTManager::pubSubCallbackWrapper(char* topic, byte* payload, unsigned int length) {
if (_instance && _instance->_callback) {
// 调用存储的用户回调函数
_instance->_callback(topic, payload, length);
}
}
// MQTT 连接尝试实现
bool MQTTManager::tryConnect() {
Serial.print("Attempting MQTT connection...");
// 使用客户端 ID、用户名和密码进行连接
if (_mqttClient.connect(_clientId.c_str(), _username.c_str(), _password.c_str())) {
Serial.println("connected!");
// 订阅主题
_mqttClient.subscribe(_subscribeTopic.c_str());
Serial.print("Subscribed to topic: ");
Serial.println(_subscribeTopic.c_str());
return true;
} else {
Serial.print("failed, rc=");
Serial.print(_mqttClient.state());
Serial.println(" try again in 5 seconds");
return false;
}
}
// loop 方法实现 (由任务调用)
void MQTTManager::loop() {
if (WiFi.status() == WL_CONNECTED) { // 只有在 WiFi 连接时才处理 MQTT
if (!_mqttClient.connected()) { // 如果 MQTT 未连接
unsigned long now = millis();
if (now - lastReconnectAttempt > 5000) {
lastReconnectAttempt = now;
if (tryConnect()) {
lastReconnectAttempt = 0;
}
}
} else {
_mqttClient.loop(); // 保持 MQTT 连接和处理消息
}
}
// 如果 WiFi 未连接,等待 WiFi 连接的任务处理
}
// 发布消息 (纯文本) 实现
bool MQTTManager::publish(const char* topic, const char* payload) {
if (_mqttClient.connected()) {
return _mqttClient.publish(topic, payload);
}
Serial.println("MQTT not connected, cannot publish.");
return false;
}
// 发布 JSON 消息实现
bool MQTTManager::publishJson(const char* action, int value) {
if (!_mqttClient.connected()) {
Serial.println("MQTT not connected, cannot publish JSON.");
return false;
}
// 创建JSON对象
DynamicJsonDocument doc(256); // 根据你的JSON文档大小调整
// 构建head部分
JsonObject head = doc.createNestedObject("head");
if(action == "audioplayend"){
head["message_type"] = 102; // 根据你的协议调整
}else if(action == "shoted"){
head["message_type"] = 101; // 根据你的协议调整
}
// 构建body部分
JsonObject body = doc.createNestedObject("body");
body["action"] = action; // 传入 action 字符串
body["val"] = value; // 传入 val 值
// 序列化JSON为字符串
String jsonString;
serializeJson(doc, jsonString);
Serial.print("Publishing JSON message to ");
Serial.print(_publishTopic.c_str());
Serial.print(": ");
Serial.println(jsonString);
// 发布消息
return _mqttClient.publish(_publishTopic.c_str(), jsonString.c_str());
}
// FreeRTOS 任务函数实现
void mqttLoopTask(void* pvParameters) {
MQTTManager* mqttManager = static_cast<MQTTManager*>(pvParameters);
if (!mqttManager) {
Serial.println("MQTT Task: Invalid parameter!");
vTaskDelete(NULL);
return;
}
for (;;) {
mqttManager->loop(); // 调用 MQTTManager 的 loop 方法
vTaskDelay(pdMS_TO_TICKS(100)); // 短暂延时
}
}
int MQTTManager::wifi_connected_is() {
return _mqttClient.state();
}
\ No newline at end of file
#ifndef MQTT_MANAGER_H
#define MQTT_MANAGER_H
#include <Arduino.h>
#include <WiFiClient.h>
#include <PubSubClient.h>
#include <ArduinoJson.h> // 需要用于发布JSON
#include <functional> // For std::function
#include <string> // For std::string
// 定义 MQTT 回调函数类型
using MqttCallbackType = std::function<void(char* topic, byte* payload, unsigned int length)>;
class MQTTManager {
private:
WiFiClient _espClient;
PubSubClient _mqttClient; // 必须在构造函数初始化列表中初始化
std::string _broker;
int _port;
std::string _username;
std::string _password;
std::string _clientId; // 使用 std::string 以便动态生成
std::string _subscribeTopic;
std::string _publishTopic;
MqttCallbackType _callback; // 存储用户提供的回调函数
unsigned long lastReconnectAttempt = 0;
// PubSubClient 的回调函数需要静态或全局
// static void staticMqttCallback(char* topic, byte* payload, unsigned int length); // 备选方案
// 使用类的 friend 任务函数或一个公共的静态方法来桥接 PubSubClient 回调
// 内部连接尝试函数
bool tryConnect();
public:
// 构造函数
// 传入 MQTT Broker 地址、端口、用户名、密码和话题
MQTTManager(const char* broker, int port, const char* username, const char* password,
const char* subscribeTopic, const char* publishTopic);
// 初始化方法
void begin();
// 设置消息回调函数
void setCallback(MqttCallbackType callback);
// 在 FreeRTOS 任务中调用,保持 MQTT 连接并处理消息
void loop();
// 发布消息 (纯文本)
bool publish(const char* topic, const char* payload);
// 发布 JSON 消息
bool publishJson(const char* action, int value);
// 检查 MQTT 连接状态
bool isConnected() { return _mqttClient.connected(); }
// 提供一个静态函数,作为 PubSubClient 的回调入口
// 这个函数会将调用转发到当前活跃的 MQTTManager 实例的实际回调函数
static void pubSubCallbackWrapper(char* topic, byte* payload, unsigned int length);
// 需要一个指向当前活跃实例的静态指针,以便回调 wrapper 可以访问
static MQTTManager* _instance;
// FreeRTOS 任务函数(需要在主文件或单独的.cpp中实现,并友元声明)
friend void mqttLoopTask(void* pvParameters);
int wifi_connected_is();
};
// FreeRTOS 任务函数声明
void mqttLoopTask(void* pvParameters);
extern MQTTManager mqttManager;
#endif // MQTT_MANAGER_H
\ No newline at end of file
#include "WiFiConfigManager.h"
#include <functional> // Needed for std::bind
// 构造函数实现
WiFiConfigManager::WiFiConfigManager(int port) : _server(port) {
// Preferences 对象不需要在这里初始化,在 begin() 或 load/save 中初始化
}
// begin 方法实现
bool WiFiConfigManager::begin() {
Serial.println("WiFiConfigManager initializing...");
// 加载保存的 WiFi 凭据
loadCredentials();
// 尝试连接 WiFi (如果凭据有效)
bool connected = false;
if (_ssid.length() > 0) {
Serial.print("Attempting to connect to saved WiFi: ");
Serial.println(_ssid);
WiFi.mode(WIFI_STA); // 设置为站点模式
WiFi.begin(_ssid.c_str(), _password.c_str());
// 等待连接或超时
int timeout_sec = 15; // 15秒超时
while (timeout_sec > 0 && WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
timeout_sec--;
}
Serial.println(); // 换行
if (WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi connected successfully!");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
connected = true;
} else {
Serial.println("WiFi connection failed.");
// 连接失败,清除凭据并进入 AP 模式
clearCredentials();
_ssid = ""; _password = ""; // 清除内存中的凭据
}
}
// 如果没有连接成功,启动 AP 配网模式
if (!connected) {
startConfigPortal();
} else {
// 如果连接成功,为了方便用户随时重新配置,也可以启动 WebServer
// 但是此时 WebServer 运行在 Station 模式下,需要通过分配的 IP 访问
//startConfigPortal(); // 启动 WebServer,但 WiFi 保持 STA 模式
if (WiFi.getMode() == WIFI_STA && WiFi.isConnected()) {
_server.on("/", std::bind(&WiFiConfigManager::handleRoot, this));
_server.on("/save", HTTP_POST, std::bind(&WiFiConfigManager::handleSave, this));
// _server.onNotFound(std::bind(&WiFiConfigManager::handleNotFound, this)); // 如果需要
// 启动 Web 服务器 (如果尚未启动)
_server.begin();
Serial.print("Web config available at http://");
Serial.println(WiFi.localIP());
}
}
return connected;
}
// loop 方法实现
void WiFiConfigManager::loop() {
_server.handleClient(); // 处理 WebServer 的客户端请求
}
// 启动 AP 配网模式实现
void WiFiConfigManager::startConfigPortal() {
Serial.println("Starting AP config portal...");
// 如果当前不是 AP 模式,切换到 AP 模式
if(WiFi.getMode() != WIFI_AP) {
WiFi.mode(WIFI_AP); // 设置 WiFi 为 AP 模式
// 启动一个名为 "ESP32C3_Config",密码为 "12345678" 的 WiFi 热点
WiFi.softAP("ESP32C3_Config", "12345678");
delay(100); // 给 AP 一点时间启动
IPAddress apIP = WiFi.softAPIP(); // 获取 AP 的 IP 地址
Serial.println("AP started!");
Serial.println("Connect to SSID: ESP32C3_Config (Password: 12345678)");
Serial.print("Open browser and visit: http://");
Serial.println(apIP);
}
// 设置 Web 服务器的路由
// 使用 std::bind 将成员函数绑定到 WebServer 回调
_server.on("/", std::bind(&WiFiConfigManager::handleRoot, this));
_server.on("/save", HTTP_POST, std::bind(&WiFiConfigManager::handleSave, this));
// _server.onNotFound(std::bind(&WiFiConfigManager::handleNotFound, this)); // 如果需要
// 启动 Web 服务器 (如果尚未启动)
_server.begin();
Serial.println("WebServer started.");
}
// 加载 WiFi 凭据实现
bool WiFiConfigManager::loadCredentials() {
Serial.println("Loading WiFi credentials from Preferences...");
_preferences.begin("wifi_config", true); // 打开 "wifi_config" 命名空间,true 表示只读模式
_ssid = _preferences.getString("ssid", ""); // 读取 "ssid"
_password = _preferences.getString("password", ""); // 读取 "password"
_preferences.end(); // 关闭 Preferences
Serial.print("Loaded SSID: '"); Serial.print(_ssid); Serial.println("'");
Serial.print("Loaded Password: '"); Serial.print(_password); Serial.println("'");
return _ssid.length() > 0; // 如果 SSID 不为空,认为加载成功
}
// 保存 WiFi 凭据实现
void WiFiConfigManager::saveCredentials() {
Serial.println("Saving WiFi credentials to Preferences...");
_preferences.begin("wifi_config", false); // 打开 "wifi_config" 命名空间,false 表示读写模式
_preferences.putString("ssid", _ssid); // 将 ssid 存入 Flash
_preferences.putString("password", _password); // 将 password 存入 Flash
_preferences.end(); // 关闭 Preferences
Serial.println("Credentials saved.");
}
// 清除 WiFi 凭据实现
void WiFiConfigManager::clearCredentials() {
Serial.println("Clearing WiFi credentials from Preferences...");
_preferences.begin("wifi_config", false);
_preferences.clear(); // 清除 "wifi_config" 命名空间下的所有数据
_preferences.end();
Serial.println("Credentials cleared.");
}
// WebServer 处理函数实现 (根目录)
void WiFiConfigManager::handleRoot() {
Serial.println("Handling / request");
_server.send(200, "text/html; charset=utf-8", _configHtml);
}
// WebServer 处理函数实现 (保存)
void WiFiConfigManager::handleSave() {
Serial.println("Handling /save request...");
if (_server.hasArg("ssid")) {
_ssid = _server.arg("ssid");
_password = _server.arg("password"); // password 可能为空
saveCredentials(); // 保存到 Flash
Serial.println("WiFi config saved. Restarting...");
// 向客户端发送成功信息
_server.send(200, "text/html; charset=utf-8", "<h3>保存成功,设备重启中...</h3><p>请等待设备重启并连接新的WiFi网络。</p>");
delay(2000); // 延迟2秒,确保浏览器收到响应
ESP.restart(); // 重启 ESP32
} else {
Serial.println("Error: Missing SSID argument in /save request.");
_server.send(400, "text/plain; charset=utf-8", "错误:缺少 SSID 参数!");
}
}
// 如果需要,可以实现 handleNotFound
// void WiFiConfigManager::handleNotFound() {
// Serial.println("Handling Not Found");
// _server.send(404, "text/plain", "Not found");
// }
\ No newline at end of file
#ifndef WIFI_CONFIG_MANAGER_H
#define WIFI_CONFIG_MANAGER_H
#include <Arduino.h>
#include <WiFi.h> // WiFi 功能库
#include <WebServer.h> // Web 服务器库,用于配网
#include <Preferences.h> // Flash 非易失性存储库,用于保存WiFi配置
#include <functional> // 需要 std::bind 或 lambda
class WiFiConfigManager {
private:
String _ssid = "";
String _password = "";
Preferences _preferences;
WebServer _server; // 必须在构造函数初始化列表中初始化
// HTML 配置页面内容(作为内部常量)
const char* _configHtml = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESP32-C3 WiFi 配网</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
h2 { color: #0056b3; }
form { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
input[type=text], input[type=password] {
width: calc(100% - 22px);
padding: 10px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
input[type=submit] {
background-color: #4CAF50;
color: white;
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
width: 100%;
}
input[type=submit]:hover {
background-color: #45a049;
}
h3 { color: #333; }
.reset-info { margin-top: 20px; font-size: 0.9em; color: #666; }
</style>
</head>
<body>
<h2>ESP32-C3 WiFi 配网</h2>
<form action="/save" method="POST">
<label for="ssid">SSID:</label><br>
<input type="text" id="ssid" name="ssid" required><br>
<label for="password">密码:</label><br>
<input type="password" id="password" name="password"><br>
<input type="submit" value="保存并重启">
</form>
<p class="reset-info">提示:长按 D10 引脚 5 秒可清除 WiFi 配置并重启设备。</p>
</body>
</html>
)rawliteral";
// WebServer 处理函数需要绑定到类的实例
void handleRoot();
void handleSave();
// void handleNotFound(); // 如果需要可以添加
// 内部辅助函数
bool loadCredentials(); // 从 Preferences 加载
void saveCredentials(); // 保存到 Preferences
public:
// 构造函数,指定 WebServer 监听端口
WiFiConfigManager(int port = 80);
// 初始化方法:加载配置,尝试连接,或启动 AP
// 返回 true 如果最终成功连接 WiFi
bool begin();
// 在 loop() 中调用,处理 WebServer 客户端
void loop();
// 启动 AP 配网模式
void startConfigPortal();
// 清除 WiFi 配置
void clearCredentials();
// 获取当前连接的 SSID 和密码 (如果已连接或加载)
String getSSID() const { return _ssid; }
String getPassword() const { return _password; }
// 检查 WiFi 连接状态
bool isWiFiConnected() const { return WiFi.status() == WL_CONNECTED; }
// 获取 WebServer 实例的引用,供需要额外路由的地方使用 (可选)
// WebServer& getServer() { return _server; }
};
#endif // WIFI_CONFIG_MANAGER_H
\ No newline at end of file
// 引入核心库
#include <WiFi.h> // WiFi 功能库
#include <WebServer.h> // Web 服务器库,用于配网
#include <Preferences.h> // Flash 非易失性存储库,用于保存WiFi配置
#include <Arduino.h>
#include <freertos/FreeRTOS.h> // FreeRTOS 实时操作系统核心库
#include <freertos/task.h> // FreeRTOS 任务管理库
// --- MQTT 库和 JSON 库 ---
#include <PubSubClient.h> // MQTT 客户端库
#include <ArduinoJson.h> // JSON 解析库 (请确保已安装最新稳定版本,如 6.x)
#include "pcm.h" // 包含你的PCM音频数据头文件
#include "driver/i2s.h" // 包含ESP32 I2S驱动头文件
// 全局对象声明
Preferences preferences; // 创建一个 Preferences 对象用于读写 Flash
WebServer server(80); // 创建一个 WebServer 对象,监听 80 端口
// 用于存储从Flash读取或Web页面获取的SSID和密码
String ssid, password;
// 引入自定义类头文件
#include "WiFiConfigManager.h"
#include "MQTTManager.h"
#include "AudioManager.h"
#include "DeviceControl.h"
// --- 按键配置 ---
const int BUTTON_PIN = D10; // D10 引脚在 ESP32C3 上对应 GPIO10
// 按键检测常量
const unsigned long LONG_PRESS_DURATION_MS = 5000; // 长按触发时间:5秒
const unsigned long DEBOUNCE_DELAY_MS = 50; // 按键去抖动时间:50毫秒
// --- 模拟读入配置 ---
const int sensorPin0 = D0; // 模拟输入引脚 D0,在 ESP32-C3 上通常是 GPIO0
......@@ -31,378 +25,45 @@ const int OUTPUT_PIN2 = D2; // 数字输出引脚 D2 -> GPIO2
const int OUTPUT_PIN3 = D3; // 数字输出引脚 D3 -> GPIO3
const int OUTPUT_PIN4 = D4; // 数字输出引脚 D4 -> GPIO4
// -- i2s -- (这些引脚已定义,但在当前代码中并未使用)
// -- I2S 引脚 --
const int I2S_BCLK = D5; // I2S 位时钟引脚
const int I2S_LRC = D6; // I2S 左右时钟引脚
const int I2S_DIN = D7; // I2S 数据输入引脚
// I2S 配置参数
const i2s_port_t I2S_PORT = I2S_NUM_0; // 使用I2S端口0
const int SAMPLE_RATE = 16000; // 音频采样率 (Hz) - 需要与你的 pcm_sound_data 匹配
// 如果播放速度不对,尝试常见的采样率如 8000, 22050, 44100
// I2S DMA 缓冲区配置
const int DMA_BUF_COUNT = 8; // DMA缓冲区数量
const int DMA_BUF_LEN = 1024; // 每个DMA缓冲区的长度 (字节)
// 全局状态变量
int ledblood = 100; // 用于控制 LED "血量" 效果的状态变量
int time_count = 0; // 通用时间计数器
int led_count = 0; // LED 闪烁效果的计数器
bool audio_index = false;
// 按键检测常量
const unsigned long LONG_PRESS_DURATION_MS = 5000; // 长按触发时间:5秒
const unsigned long DEBOUNCE_DELAY_MS = 50; // 按键去抖动时间:50毫秒
const int I2S_DIN = D7; // I2S 数据输入引脚 (MAX98357A 不使用)
// --- MQTT 配置 ---
// 【错误修正】: C++中定义字符串常量需要使用 const char* 和双引号 ""
const char* MQTT_BROKER = "119.45.167.177"; // 你的 MQTT Broker IP 或 Hostname
const int MQTT_PORT = 1883; // MQTT 端口 (通常是 1883 或 8883 for SSL)
const char* MQTT_USERNAME = "admin"; // -- 新增:你的 MQTT 用户名
const char* MQTT_PASSWORD = "admin"; // -- 新增:你的 MQTT 密码
String MQTT_CLIENT_ID; // 客户端ID必须是唯一的,这里使用MAC地址的一部分确保唯一性
const int MQTT_PORT = 1883; // MQTT 端口
const char* MQTT_USERNAME = "admin"; // 你的 MQTT 用户名
const char* MQTT_PASSWORD = "admin"; // 你的 MQTT 密码
const char* MQTT_SUBSCRIBE_TOPIC = "ser2dev/13990100000001"; // 你要订阅的 MQTT 主题
const char* MQTT_RELEASE_TOPIC = "dev2ser/13990100000001"; // 你要发布的 MQTT 主题
// --- 创建类实例 ---
// 这些实例现在包含了之前分散的全局变量和相关函数
WiFiConfigManager wifiManager; // 使用默认端口 80
MQTTManager mqttManager(MQTT_BROKER, MQTT_PORT, MQTT_USERNAME, MQTT_PASSWORD, MQTT_SUBSCRIBE_TOPIC, MQTT_RELEASE_TOPIC);
AudioManager audioManager(I2S_BCLK, I2S_LRC, I2S_DIN); // 使用默认采样率和缓冲区配置
DeviceControl deviceControl(OUTPUT_PIN1, OUTPUT_PIN2, OUTPUT_PIN3, OUTPUT_PIN4, sensorPin0);
// WiFi客户端用于MQTT
WiFiClient espClient;
PubSubClient mqttClient(espClient); // 声明 MQTT 客户端对象
// HTML 页面用于输入 SSID 和密码
// 【格式修正】: 在 HTML 标签和属性之间添加了空格,以确保浏览器正确解析
// R"rawliteral(...)rawliteral" 是 C++ 的原始字符串字面量,可以方便地包含特殊字符而无需转义
const char config_html[] = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"> <!-- 关键:确保浏览器以 UTF-8 编码解析页面 -->
<title>ESP32-C3 WiFi 配网</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
h2 { color: #0056b3; }
form { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
input[type=text], input[type=password] {
width: calc(100% - 22px);
padding: 10px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
input[type=submit] {
background-color: #4CAF50;
color: white;
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
width: 100%;
}
input[type=submit]:hover {
background-color: #45a049;
}
h3 { color: #333; }
.reset-info { margin-top: 20px; font-size: 0.9em; color: #666; }
</style>
</head>
<body>
<h2>ESP32-C3 WiFi 配网</h2>
<form action="/save" method="POST">
<label for="ssid">SSID:</label><br>
<input type="text" id="ssid" name="ssid" required><br>
<label for="password">密码:</label><br>
<input type="password" id="password" name="password"><br>
<input type="submit" value="保存并重启">
</form>
<p class="reset-info">提示:长按 D10 引脚 5 秒可清除 WiFi 配置并重启设备。</p>
</body>
</html>
)rawliteral";
// 处理根目录请求,显示配网页面
void handleRoot() {
// 发送 200 OK 状态码和 HTML 页面内容
server.send(200, "text/html; charset=utf-8", config_html);
}
// 处理保存 WiFi 配置的请求
void handleSave() {
// 检查请求中是否包含 "ssid" 和 "password" 参数
if (server.hasArg("ssid")) {
ssid = server.arg("ssid");
password = server.arg("password");
// 【错误修正】: Preferences 的命名空间必须是字符串
preferences.begin("wifi_config", false); // 打开 "wifi_config" 命名空间,false 表示读写模式
preferences.putString("ssid", ssid); // 将 ssid 存入 Flash
preferences.putString("password", password); // 将 password 存入 Flash
preferences.end(); // 关闭 Preferences
Serial.println("WiFi 配置已保存。SSID: " + ssid + ", Password: " + password);
// 向客户端发送成功信息
server.send(200, "text/html; charset=utf-8", "<h3>保存成功,设备重启中...</h3><p>请等待设备重启并连接新的WiFi网络。</p>");
delay(2000); // 延迟2秒,确保浏览器收到响应
ESP.restart(); // 重启 ESP32
} else {
// 如果缺少参数,返回 400 Bad Request 错误
server.send(400, "text/plain; charset=utf-8", "错误:缺少 SSID 参数!");
}
}
// 启动 AP (Access Point) 配网模式
void startConfigPortal() {
WiFi.mode(WIFI_AP); // 设置 WiFi 为 AP 模式
// 启动一个名为 "ESP32C3_Config",密码为 "12345678" 的 WiFi 热点
WiFi.softAP("ESP32C3_Config", "12345678");
IPAddress IP = WiFi.softAPIP(); // 获取 AP 的 IP 地址
Serial.println("\nAP 配网模式启动!");
Serial.println("请连接 Wi-Fi 热点: ESP32C3_Config (密码: 12345678)");
Serial.print("在浏览器中访问以下地址进行配网: http://");
Serial.println(IP);
// 设置 Web 服务器的路由
server.on("/", handleRoot); // 根目录 (/) 路由到 handleRoot 函数
server.on("/save", HTTP_POST, handleSave); // /save 路径的 POST 请求路由到 handleSave 函数
server.begin(); // 启动 Web 服务器
}
// 从 Flash 读取保存的 WiFi 信息
bool loadWiFiConfig(String& ssid_out, String& password_out) {
// 【错误修正】: Preferences 的命名空间必须是字符串,true 表示只读模式
preferences.begin("wifi_config", true);
// 【错误修正】: getString 的 key 必须是字符串
ssid_out = preferences.getString("ssid", ""); // 读取 "ssid",如果不存在则返回空字符串
password_out = preferences.getString("password", ""); // 读取 "password"
preferences.end();
Serial.print("从Flash读取到的SSID: '"); Serial.print(ssid_out); Serial.println("'");
Serial.print("从Flash读取到的密码: '"); Serial.print(password_out); Serial.println("'");
// 如果 SSID 长度大于0,则认为配置有效
return ssid_out.length() > 0;
}
// 启动 STA (Station) 模式连接 WiFi
bool connectWiFi() {
String ssid_str, password_str;
if (!loadWiFiConfig(ssid_str, password_str)) {
Serial.println("Flash中没有保存的WiFi配置。");
return false; // 如果没有配置,直接返回失败
}
WiFi.mode(WIFI_STA); // 设置 WiFi 为 STA 模式
WiFi.begin(ssid_str.c_str(), password_str.c_str()); // 开始连接
Serial.print("正在尝试连接 WiFi: ");
Serial.println(ssid_str);
// 最多尝试连接 30 次 (每次500ms,总共15秒)
for (int i = 0; i < 30; i++) {
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi 连接成功!");
Serial.print("已连接到: ");
Serial.println(WiFi.SSID());
Serial.print("设备 IP 地址: ");
Serial.println(WiFi.localIP());
return true; // 连接成功
}
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi 连接失败。");
return false; // 连接失败
}
// 启动后台网页用于重新配网(WiFi连接成功后)
void startConfigWebPortal() {
// 设置与 AP 模式下相同的路由,以便在连接 WiFi 后也能重新配置
server.on("/", handleRoot);
server.on("/save", HTTP_POST, handleSave);
server.begin(); // 启动 Web 服务器
Serial.print("后台网页服务已启动,您可以通过 IP 地址: ");
Serial.print(WiFi.localIP());
Serial.println(" 再次进行配网。");
}
void setup_i2s() {
// 1. 配置I2S总线
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), // 设置为主机模式,发送数据
.sample_rate = SAMPLE_RATE, // 设置采样率
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 设置每个采样点的位数 (PCM数据通常是16位)
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // MAX98357是单声道,但I2S通常配置为双声道,
// 如果你的PCM数据是单声道,它会被复制到左右声道。
// 或者如果PCM数据本身已经是双声道(左声道数据后跟右声道数据)
// 或者可以尝试 I2S_CHANNEL_FMT_ALL_LEFT 或 I2S_CHANNEL_FMT_ALL_RIGHT 如果你的数据是纯单声道
.communication_format = I2S_COMM_FORMAT_STAND_I2S, // 标准I2S通信格式 (MSB优先)
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // 中断分配标志 (默认)
.dma_buf_count = DMA_BUF_COUNT, // DMA缓冲区数量
.dma_buf_len = DMA_BUF_LEN, // 每个DMA缓冲区的长度 (字节)
.use_apll = false, // 不使用APLL (音频锁相环)
.tx_desc_auto_clear = true, // 自动清除已发送的TX描述符
.fixed_mclk = 0 // 不使用固定的MCLK (MAX98357不需要MCLK)
};
// 2. 安装I2S驱动
esp_err_t err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
if (err != ESP_OK) {
Serial.printf("I2S驱动安装失败: %d\n", err);
return;
}
Serial.println("I2S驱动已安装");
// 3. 配置I2S引脚
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_BCLK, // 位时钟引脚
.ws_io_num = I2S_LRC, // 左右时钟/字选择引脚
.data_out_num = I2S_DIN, // 数据输出引脚
.data_in_num = I2S_PIN_NO_CHANGE // 不使用数据输入引脚
};
err = i2s_set_pin(I2S_PORT, &pin_config);
if (err != ESP_OK) {
Serial.printf("I2S引脚设置失败: %d\n", err);
return;
}
Serial.println("I2S引脚已设置");
}
void play_audio() {
Serial.println("开始播放音频...");
size_t bytes_written = 0;
int current_pos = 0;
// 创建一个RAM缓冲区,用于从PROGMEM拷贝数据块
// pcm_sound_data 存储在PROGMEM (Flash)中,不能直接传递给i2s_write
// DMA_BUF_LEN 是一个合适的块大小
uint8_t ram_buffer[DMA_BUF_LEN];
// 循环直到所有音频数据都被发送
while (current_pos < pcm_sound_data_len) {
// 计算本次要拷贝的字节数
int bytes_to_copy = DMA_BUF_LEN;
if (current_pos + bytes_to_copy > pcm_sound_data_len) {
bytes_to_copy = pcm_sound_data_len - current_pos;
}
// 从PROGMEM拷贝数据到RAM缓冲区
// pcm_sound_data 是 const unsigned char[] 数组,它在 pcm.h 中定义
// memcpy_P 用于从PROGMEM拷贝数据
memcpy_P(ram_buffer, &pcm_sound_data[current_pos], bytes_to_copy);
// 将RAM缓冲区中的数据写入I2S总线
// i2s_write 会阻塞直到数据被发送或超时
esp_err_t result = i2s_write(I2S_PORT, ram_buffer, bytes_to_copy, &bytes_written, portMAX_DELAY);
if (result != ESP_OK) {
Serial.printf("I2S写入错误: %d\n", result);
break;
}
if (bytes_written < bytes_to_copy) {
Serial.printf("I2S写入不足: 只写入了 %d / %d 字节\n", bytes_written, bytes_to_copy);
// 可以选择中断或继续,这里我们继续
}
current_pos += bytes_written; // 更新当前位置
// Serial.printf("已发送 %d / %d 字节\n", current_pos, pcm_sound_data_len); // 调试信息
}
// (可选)播放完毕后,可以发送一些静音数据以确保DAC输出静音
// 或者清空DMA缓冲区
i2s_zero_dma_buffer(I2S_PORT);
Serial.println("音频播放完毕。");
}
// GPIO 输出控制函数
void pin_value(int pin ,int val) {
if(val == 1){ // 如果值为 1,则输出高电平
if(pin == 1) digitalWrite(OUTPUT_PIN1, HIGH);
if(pin == 2) digitalWrite(OUTPUT_PIN2, HIGH);
if(pin == 3) digitalWrite(OUTPUT_PIN3, HIGH);
if(pin == 4) digitalWrite(OUTPUT_PIN4, HIGH);
}else{ // 否则输出低电平
if(pin == 1) digitalWrite(OUTPUT_PIN1, LOW);
if(pin == 2) digitalWrite(OUTPUT_PIN2, LOW);
if(pin == 3) digitalWrite(OUTPUT_PIN3, LOW);
if(pin == 4) digitalWrite(OUTPUT_PIN4, LOW);
}
}
// PWM 输出控制函数
void pwm_value(int pin ,int val) {
// 【错误修正】: "val2.55" 是无效语法,应为 "val * 2.55"
// analogWrite 在 ESP32 上是 8 位分辨率 (0-255),此函数将输入的 0-100 值映射到 0-255
if(pin == 1) analogWrite(OUTPUT_PIN1, val * 2.55);
if(pin == 2) analogWrite(OUTPUT_PIN2, val * 2.55);
if(pin == 3) analogWrite(OUTPUT_PIN3, val * 2.55);
if(pin == 4) analogWrite(OUTPUT_PIN4, val * 2.55);
}
// 控制 LED "血量" 效果的函数
void led_blood_count(){
if(ledblood == 6) { // 满血状态,3个灯全亮
pin_value(1,1);
pin_value(2,1);
pin_value(3,1);
}else if(ledblood == 5){ // 5格血,灯1闪烁,灯2、3常亮
if(led_count < 10) pin_value(1,1);
if(led_count >= 10){
pin_value(1,0);
if(led_count > 20) led_count = 0;
}
pin_value(2,1);
pin_value(3,1);
}else if(ledblood == 4){ // 4格血,灯1灭,灯2、3常亮
pin_value(1,0);
pin_value(2,1);
pin_value(3,1);
}else if(ledblood == 3){ // 3格血,灯2闪烁,灯3常亮,灯1灭
if(led_count < 10) pin_value(2,1);
if(led_count >= 10){
pin_value(2,0);
if(led_count > 20) led_count = 0;
}
pin_value(1,0);
pin_value(3,1);
}else if(ledblood == 2){ // 2格血,灯3常亮,灯1、2灭
pin_value(1,0);
pin_value(2,0);
pin_value(3,1);
}else if(ledblood == 1){ // 1格血,灯3闪烁,灯1、2灭
if(led_count < 10) pin_value(3,1);
if(led_count >= 10){
pin_value(3,0);
if(led_count > 20) led_count = 0;
}
pin_value(1,0);
pin_value(2,0);
}else if(ledblood == 0){ // 【注释】: 原始代码为 ledblood == 2,这里假设意图是0。如果仍是2,此逻辑块将被上面的 ledblood == 2 覆盖。
pin_value(1,0);
pin_value(2,0);
pin_value(3,0);
}
}
// --- FreeRTOS 任务声明 ---
// 任务函数实现会放在 setup 或 loop 函数之后
void buttonMonitorTask(void* pvParameters); // 按键监控任务
// mqttLoopTask 在 MQTTManager.cpp 中声明和实现
// audioPlayTask 在 AudioManager.cpp 中声明和实现
// devicePeriodicTask 在 DeviceControl.cpp 中声明和实现
// --- MQTT 消息回调函数 ---
// 【错误修正】: 修正了回调函数的正确签名 (char* topic, byte* payload, unsigned int length)
void mqttCallback(char* topic, byte* payload, unsigned int length) {
Serial.print("收到 MQTT 消息 - 主题: ");
// 这个函数现在处理接收到的 MQTT 消息,并调用相应的类方法
void handleMqttMessage(char* topic, byte* payload, unsigned int length) {
Serial.print("Received MQTT message - Topic: ");
Serial.print(topic);
Serial.print(", 内容: ");
// 【错误修正】: payload 是 byte*,需要正确转换为 char* 才能用于字符串操作
// 同时确保字符串以空字符 '\0' 结尾
Serial.print(", Payload: ");
// 复制 payload 到一个以 null 结尾的字符串
char messagePayload[length + 1];
strncpy(messagePayload, (char*)payload, length);
messagePayload[length] = '\0'; // 确保字符串以null结尾
messagePayload[length] = '\0';
Serial.println(messagePayload);
// --- 解析 JSON 数据 ---
......@@ -411,160 +72,104 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) {
DeserializationError error = deserializeJson(doc, messagePayload);
if (error) {
Serial.print("JSON 解析失败: ");
Serial.print("JSON parsing failed: ");
Serial.println(error.c_str());
return;
}
// 1. 解析 head 部分
int message_type = 0;
// 【错误修正】: JSON中的键(key)必须是字符串
if (doc.containsKey("head")) {
// 获取 head 对象
JsonObject head = doc["head"];
// 从 head 对象中提取 message_type
JsonObject head = doc["head"];
if (head.containsKey("message_type")) {
message_type = head["message_type"];
Serial.print(" - 消息类型 (head.message_type): ");
Serial.print(" - Message Type (head.message_type): ");
Serial.println(message_type);
}
}
// 2. 解析 body 部分
// 2. 解析 body 部分并分发给相应的管理器
if (doc.containsKey("body")) {
// 获取 body 对象
JsonObject body = doc["body"];
if( message_type == 3 || message_type == 4){ // GPIO 或 PWM 控制
int pin=0, val=0;
// 从 body 对象中提取 pin
if (body.containsKey("pin")) {
pin = body["pin"];
Serial.print(" - 引脚 (body.pin): ");
Serial.println(pin);
}
// 从 body 对象中提取 val
if (body.containsKey("val")) {
val = body["val"];
Serial.print(" - 值 (body.val): ");
Serial.println(val);
}
if( message_type == 3 ) { // 数字IO控制
pin_value(pin, val);
}else if(message_type == 4 ){ // PWM控制
pwm_value(pin, val);
}
Serial.print("pin=");
Serial.print(pin);
Serial.print(", val=");
Serial.println(val);
} else if( message_type == 5){ // 动作控制
int val=0;
// 从 body 对象中提取 val
if (body.containsKey("val")) {
val = body["val"];
Serial.print(" - 值 (body.val): ");
Serial.println(val);
}
if (body.containsKey("action")) {
// 【错误修正】: 字符串比较需要使用双引号
if(body["action"] == "led"){
led_count=0; // 重置LED闪烁计数器
ledblood = val; // 更新血量值
}else if(body["action"] == "fog_ms"){
time_count=0; // 重置时间计数器 (此功能未完全实现)
}else if(body["action"] == "audio"){
audio_index=val; // 重置时间计数器 (此功能未完全实现)
}
Serial.print("action: ");
Serial.println(body["action"].as<String>()); // 使用 as<String>() 安全地获取字符串值
}
}
// 交给 DeviceControl 处理 GPIO/PWM 和 LED 血量控制
deviceControl.handleMQTTCommand(message_type, body);
// 如果是 audio 动作,请求 AudioManager 播放
// if (message_type == 5 && body.containsKey("action") && body["action"].as<String>() == "audio") {
// int val = body["val"] | 0; // Get value, default to 0 if not present
// if (val == 1) { // Assuming val=1 means request playback
// audioManager.requestPlay(); // 请求音频播放
// }
// Serial.printf("Processed action: audio, val=%d\n", val);
// }
}
}
// --- MQTT 连接和订阅尝试 (不阻塞,供任务调用) ---
bool tryMqttConnect() {
// 如果 MQTT 客户端 ID 为空,则生成一个
if (MQTT_CLIENT_ID.isEmpty()) {
MQTT_CLIENT_ID = "ESP32C3-" + WiFi.macAddress().substring(9);
MQTT_CLIENT_ID.replace(":", ""); // 移除MAC地址中的冒号
Serial.print("生成 MQTT Client ID: ");
Serial.println(MQTT_CLIENT_ID);
}
void setup() {
Serial.begin(115200); // 启动串口通信
delay(1000); // 等待串口稳定
Serial.println("\n--- System Starting ---");
//mqtt初始化
mqttManager.begin();
// 设置各管理器之间的依赖关系 (通过设置指针)
audioManager.setMQTTManager(&mqttManager);
deviceControl.setMQTTManager(&mqttManager);
mqttManager.setCallback(handleMqttMessage); // 设置 MQTT 回调函数
// 初始化设备控制(引脚模式等)
deviceControl.begin();
Serial.print("尝试连接 MQTT Broker...");
// --- 关键修改:使用带有用户名和密码的 connect 方法 ---
if (mqttClient.connect(MQTT_CLIENT_ID.c_str(), MQTT_USERNAME, MQTT_PASSWORD)) {
Serial.println("成功连接 MQTT Broker!");
// 连接成功后,订阅指定的主题
mqttClient.subscribe(MQTT_SUBSCRIBE_TOPIC);
Serial.print("已订阅主题: ");
Serial.println(MQTT_SUBSCRIBE_TOPIC);
return true; // 返回成功
} else {
Serial.print("MQTT 连接失败,返回码: ");
Serial.println(mqttClient.state()); // 打印失败状态码
return false; // 返回失败
}
}
//按键初始化
pinMode(BUTTON_PIN, INPUT_PULLUP);
// 初始化音频管理器 (I2S)
audioManager.begin();
// 初始化 WiFi 配置管理器 (尝试连接或启动 AP)
bool wifiConnected = wifiManager.begin();
// 创建 FreeRTOS 任务
// 任务名称, 栈大小, 参数, 优先级, 任务句柄
xTaskCreate(buttonMonitorTask, "ButtonMonitor", 2048, (void*)&wifiManager, 1, NULL); // 将 wifiManager 实例地址传递给按键任务
xTaskCreate(mqttLoopTask, "MQTTLoop", 4096, (void*)&mqttManager, 1, NULL); // 将 mqttManager 实例地址传递给 MQTT 任务
xTaskCreate(audioPlayTask, "AudioPlay", 2048, (void*)&audioManager, 1, NULL); // 将 audioManager 实例地址传递给音频任务
xTaskCreate(devicePeriodicTask, "DevicePeriodic", 2048, (void*)&deviceControl, 1, NULL); // 将 deviceControl 实例地址传递给设备任务
// 启动时播放一次声音(指示启动完成或AP模式启动)
audioManager.requestPlay(); // 请求播放启动音
// --- FreeRTOS MQTT 任务 ---
void mqttTask(void* pvParameters) {
// 设置 MQTT 服务器和端口
mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
// 设置消息回调函数
mqttClient.setCallback(mqttCallback);
for (;;) { // 无限循环,这是 FreeRTOS 任务的标准写法
if (WiFi.status() == WL_CONNECTED) { // 只有在 WiFi 连接时才处理 MQTT
if (!mqttClient.connected()) { // 如果 MQTT 未连接
Serial.println("MQTT Task: MQTT 连接断开,尝试重连...");
if (!tryMqttConnect()) { // 尝试重连
Serial.println("MQTT Task: 重连失败,等待下次尝试...");
vTaskDelay(pdMS_TO_TICKS(5000)); // 失败时等待 5 秒后重试
}
} else {
mqttClient.loop(); // 保持 MQTT 连接和处理消息
}
} else {
Serial.println("MQTT Task: WiFi 未连接,无法连接 MQTT。等待 WiFi...");
}
// 任务延时,让出 CPU 给其他任务,并避免繁忙循环
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 发布 JSON 格式的 MQTT 消息
void publishJsonMessage() {
// 创建JSON对象
DynamicJsonDocument doc(256);
// 构建head部分
// 【错误修正】: JSON key 必须是字符串
JsonObject head = doc.createNestedObject("head");
head["message_type"] = 101;
// 构建body部分
JsonObject body = doc.createNestedObject("body");
body["action"] = "shoted";
body["val"] = 1;
// 序列化JSON为字符串
String jsonString;
serializeJson(doc, jsonString);
// 发布消息
mqttClient.publish(MQTT_RELEASE_TOPIC, jsonString.c_str());
Serial.println("发布消息:");
Serial.println(jsonString);
void loop() {
// 主循环中主要处理 Web 服务器客户端请求
// 其他周期性或阻塞性任务都在 FreeRTOS 任务中运行
wifiManager.loop();
// 短暂延时,避免主循环空转
delay(10);
}
// --- FreeRTOS 按键监控任务 ---
// --- FreeRTOS 任务函数实现 ---
// 按键监控任务实现
void buttonMonitorTask(void* pvParameters) {
int lastButtonState = HIGH; // 上一次按键状态 (HIGH 未按下, LOW 已按下)
// 从参数中获取 WiFiConfigManager 实例指针
WiFiConfigManager* wifiManager = static_cast<WiFiConfigManager*>(pvParameters);
if (!wifiManager) {
Serial.println("Button Monitor Task: Invalid WiFiManager parameter!");
vTaskDelete(NULL); // 删除当前任务
return;
}
int lastButtonState = HIGH; // 上一次按键状态
unsigned long buttonPressStartTime = 0; // 按键按下时的 millis() 时间
bool resetTriggered = false; // 标志,防止长按期间重复触发
......@@ -573,164 +178,45 @@ void buttonMonitorTask(void* pvParameters) {
// --- 按键去抖动 ---
if (currentButtonState != lastButtonState) {
vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_DELAY_MS));
vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_DELAY_MS)); // 等待去抖动时间
currentButtonState = digitalRead(BUTTON_PIN); // 再次读取以确认状态
}
if (currentButtonState == LOW) { // 如果按键被按下
if (lastButtonState == HIGH) { // 如果是刚按下的瞬间
buttonPressStartTime = millis();
resetTriggered = false;
Serial.println("按键 D10 被按下...");
resetTriggered = false; // Reset trigger flag on new press
Serial.println("Button D10 pressed...");
}
// --- 检测长按 ---
if (!resetTriggered && (millis() - buttonPressStartTime >= LONG_PRESS_DURATION_MS)) {
Serial.println("长按 D10 超过 5 秒检测到!正在清除 WiFi 配置...");
//听到声音表示长按成功
play_audio();
// 【错误修正】: Preferences 命名空间必须是字符串
preferences.begin("wifi_config", false);
preferences.clear(); // 清除 "wifi_config" 命名空间下的所有数据
preferences.end();
Serial.println("WiFi 配置已清除。设备即将重启...");
resetTriggered = true; // 标记已触发,防止重复重启
delay(100); // 短暂延时
if (!resetTriggered && ((millis() - buttonPressStartTime) >= LONG_PRESS_DURATION_MS)) {
Serial.println("Long press detected on D10! Clearing WiFi config...");
// 听到声音表示长按成功
audioManager.requestPlay(); // 请求播放声音
// 清除 WiFi 配置并重启
wifiManager->clearCredentials();
Serial.println("WiFi Config cleared. Device restarting...");
resetTriggered = true; // Mark as triggered
vTaskDelay(pdMS_TO_TICKS(100)); // 短暂延时
ESP.restart(); // 重启设备
}
} else { // 如果按键未被按下
if (lastButtonState == LOW) { // 如果是刚松开的瞬间
if (!resetTriggered && (millis() - buttonPressStartTime < LONG_PRESS_DURATION_MS)) {
Serial.println("按键 D10 短按 (未触发重置)。");
}
// 短按逻辑(如果需要)可以在这里实现:检查 lastButtonState == LOW 且 resetTriggered == false
if (lastButtonState == LOW && !resetTriggered) {
// Serial.println("Short press detected on D10."); // uncomment if you want short press logic
// Add short press action here if needed
}
buttonPressStartTime = 0; // 重置计时器
buttonPressStartTime = 0; // Reset timer
}
lastButtonState = currentButtonState; // 更新按键状态
// 【错误修正】: "2.54095"前缺少运算符,且 "sensor_val =" 是赋值不是比较
// 假设意图是乘法和大于等于比较
const double sensor_val = analogRead(sensorPin0) * 2.5 / 4095.0; // 示例:将读数转换为电压(假设2.5V参考)
if(sensor_val >= 1.25 ) publishJsonMessage(); // 当传感器值超过阈值时发布消息
// 调用LED控制函数
led_blood_count();
// 更新计数器,并防止溢出
time_count++; if(time_count > 1000) time_count=1001; // 【错误修正】: 添加比较运算符
led_count++; if(led_count > 1000) led_count=1001; // 【错误修正】: 添加比较运算符
// 任务延时50毫秒
vTaskDelay(pdMS_TO_TICKS(50));
}
}
//音频播放任务
void audiopalyTask(void* pvParameters)
{
while(1){
if(audio_index==true){
play_audio();
audio_index=false;
// 创建JSON对象
DynamicJsonDocument doc(256);
// 构建head部分
// 【错误修正】: JSON key 必须是字符串
JsonObject head = doc.createNestedObject("head");
head["message_type"] = 102;
// 构建body部分
JsonObject body = doc.createNestedObject("body");
body["action"] = "audioplayend";
body["val"] = 1;
// 序列化JSON为字符串
String jsonString;
serializeJson(doc, jsonString);
// 发布消息
mqttClient.publish(MQTT_RELEASE_TOPIC, jsonString.c_str());
Serial.println("发布消息:");
Serial.println(jsonString);
vTaskDelay(pdMS_TO_TICKS(50)); // Task delay
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void setup() {
Serial.begin(115200); // 启动串口通信,波特率为 115200
delay(1000); // 等待串口稳定
Serial.println("\n--- 系统启动 ---");
// 初始化I2S
setup_i2s();
// 配置引脚模式
pinMode(BUTTON_PIN, INPUT_PULLUP); // 按键引脚设为上拉输入
pinMode(sensorPin0, INPUT); // 传感器引脚设为输入
pinMode(OUTPUT_PIN1, OUTPUT); // 输出引脚设为输出
pinMode(OUTPUT_PIN2, OUTPUT);
pinMode(OUTPUT_PIN3, OUTPUT);
pinMode(OUTPUT_PIN4, OUTPUT);
// 创建并启动按键监控任务
xTaskCreate(
buttonMonitorTask,
"ButtonMonitor", // 任务名称(字符串)
2048, // 任务栈空间大小 (字节)
NULL, // 传递给任务的参数
1, // 任务优先级
NULL // 任务句柄
);
// 创建并启动音频播放任务
xTaskCreate(
audiopalyTask,
"audiopaly", // 任务名称(字符串)
2048, // 任务栈空间大小 (字节)
NULL, // 传递给任务的参数
1, // 任务优先级
NULL // 任务句柄
);
// 尝试连接之前保存的 WiFi 网络
if (connectWiFi()) {
play_audio();//连接成功播放一声
// 如果连接成功,启动后台网页服务以便重新配网
startConfigWebPortal();
// 在 WiFi 连接成功后,创建并启动 MQTT 任务
// 【错误修正】: 任务名称应为字符串
xTaskCreate(
mqttTask,
"MQTTTask", // 任务名称(字符串)
4096, // 任务栈空间大小 (MQTT和JSON解析可能需要更多栈,建议4096字节或更多)
NULL, // 传递给任务的参数
1, // 任务优先级
NULL // 任务句柄
);
} else {
play_audio();
delay(2000);
play_audio();
// 如果连接失败,启动 AP 配网模式
startConfigPortal();
}
}
void loop() {
// 主循环中只需要处理 Web 服务器的客户端请求
// 其他所有功能(按键、MQTT)都在后台的 FreeRTOS 任务中运行
server.handleClient();
}
\ No newline at end of file
// MQTTLoopTask, AudioPlayTask, DevicePeriodicTask 的实现已经分别放在各自的 .cpp 文件中
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment