add some code
This commit is contained in:
81
main/boards/common/adc_battery_monitor.cc
Normal file
81
main/boards/common/adc_battery_monitor.cc
Normal file
@@ -0,0 +1,81 @@
|
||||
#include "adc_battery_monitor.h"
|
||||
|
||||
AdcBatteryMonitor::AdcBatteryMonitor(adc_unit_t adc_unit, adc_channel_t adc_channel, float upper_resistor, float lower_resistor, gpio_num_t charging_pin)
|
||||
: charging_pin_(charging_pin) {
|
||||
|
||||
// Initialize charging pin
|
||||
gpio_config_t gpio_cfg = {
|
||||
.pin_bit_mask = 1ULL << charging_pin,
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
ESP_ERROR_CHECK(gpio_config(&gpio_cfg));
|
||||
|
||||
// Initialize ADC battery estimation
|
||||
adc_battery_estimation_t adc_cfg = {
|
||||
.internal = {
|
||||
.adc_unit = adc_unit,
|
||||
.adc_bitwidth = ADC_BITWIDTH_12,
|
||||
.adc_atten = ADC_ATTEN_DB_12,
|
||||
},
|
||||
.adc_channel = adc_channel,
|
||||
.upper_resistor = upper_resistor,
|
||||
.lower_resistor = lower_resistor
|
||||
};
|
||||
adc_cfg.charging_detect_cb = [](void *user_data) -> bool {
|
||||
AdcBatteryMonitor *self = (AdcBatteryMonitor *)user_data;
|
||||
return gpio_get_level(self->charging_pin_) == 1;
|
||||
};
|
||||
adc_cfg.charging_detect_user_data = this;
|
||||
adc_battery_estimation_handle_ = adc_battery_estimation_create(&adc_cfg);
|
||||
|
||||
// Initialize timer
|
||||
esp_timer_create_args_t timer_cfg = {
|
||||
.callback = [](void *arg) {
|
||||
AdcBatteryMonitor *self = (AdcBatteryMonitor *)arg;
|
||||
self->CheckBatteryStatus();
|
||||
},
|
||||
.arg = this,
|
||||
.name = "adc_battery_monitor",
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&timer_cfg, &timer_handle_));
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000));
|
||||
}
|
||||
|
||||
AdcBatteryMonitor::~AdcBatteryMonitor() {
|
||||
if (adc_battery_estimation_handle_) {
|
||||
ESP_ERROR_CHECK(adc_battery_estimation_destroy(adc_battery_estimation_handle_));
|
||||
}
|
||||
}
|
||||
|
||||
bool AdcBatteryMonitor::IsCharging() {
|
||||
bool is_charging = false;
|
||||
ESP_ERROR_CHECK(adc_battery_estimation_get_charging_state(adc_battery_estimation_handle_, &is_charging));
|
||||
return is_charging;
|
||||
}
|
||||
|
||||
bool AdcBatteryMonitor::IsDischarging() {
|
||||
return !IsCharging();
|
||||
}
|
||||
|
||||
uint8_t AdcBatteryMonitor::GetBatteryLevel() {
|
||||
float capacity = 0;
|
||||
ESP_ERROR_CHECK(adc_battery_estimation_get_capacity(adc_battery_estimation_handle_, &capacity));
|
||||
return capacity;
|
||||
}
|
||||
|
||||
void AdcBatteryMonitor::OnChargingStatusChanged(std::function<void(bool)> callback) {
|
||||
on_charging_status_changed_ = callback;
|
||||
}
|
||||
|
||||
void AdcBatteryMonitor::CheckBatteryStatus() {
|
||||
bool new_charging_status = IsCharging();
|
||||
if (new_charging_status != is_charging_) {
|
||||
is_charging_ = new_charging_status;
|
||||
if (on_charging_status_changed_) {
|
||||
on_charging_status_changed_(is_charging_);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
main/boards/common/adc_battery_monitor.h
Normal file
30
main/boards/common/adc_battery_monitor.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef ADC_BATTERY_MONITOR_H
|
||||
#define ADC_BATTERY_MONITOR_H
|
||||
|
||||
#include <functional>
|
||||
#include <driver/gpio.h>
|
||||
#include <adc_battery_estimation.h>
|
||||
#include <esp_timer.h>
|
||||
|
||||
class AdcBatteryMonitor {
|
||||
public:
|
||||
AdcBatteryMonitor(adc_unit_t adc_unit, adc_channel_t adc_channel, float upper_resistor, float lower_resistor, gpio_num_t charging_pin = GPIO_NUM_NC);
|
||||
~AdcBatteryMonitor();
|
||||
|
||||
bool IsCharging();
|
||||
bool IsDischarging();
|
||||
uint8_t GetBatteryLevel();
|
||||
|
||||
void OnChargingStatusChanged(std::function<void(bool)> callback);
|
||||
|
||||
private:
|
||||
gpio_num_t charging_pin_;
|
||||
adc_battery_estimation_handle_t adc_battery_estimation_handle_ = nullptr;
|
||||
esp_timer_handle_t timer_handle_ = nullptr;
|
||||
bool is_charging_ = false;
|
||||
std::function<void(bool)> on_charging_status_changed_;
|
||||
|
||||
void CheckBatteryStatus();
|
||||
};
|
||||
|
||||
#endif // ADC_BATTERY_MONITOR_H
|
||||
371
main/boards/common/afsk_demod.cc
Normal file
371
main/boards/common/afsk_demod.cc
Normal file
@@ -0,0 +1,371 @@
|
||||
#include "afsk_demod.h"
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include "esp_log.h"
|
||||
#include "display.h"
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
namespace audio_wifi_config
|
||||
{
|
||||
static const char *kLogTag = "AUDIO_WIFI_CONFIG";
|
||||
|
||||
void ReceiveWifiCredentialsFromAudio(Application *app,
|
||||
WifiConfigurationAp *wifi_ap,
|
||||
Display *display,
|
||||
size_t input_channels
|
||||
)
|
||||
{
|
||||
const int kInputSampleRate = 16000; // Input sampling rate
|
||||
const float kDownsampleStep = static_cast<float>(kInputSampleRate) / static_cast<float>(kAudioSampleRate); // Downsampling step
|
||||
std::vector<int16_t> audio_data;
|
||||
AudioSignalProcessor signal_processor(kAudioSampleRate, kMarkFrequency, kSpaceFrequency, kBitRate, kWindowSize);
|
||||
AudioDataBuffer data_buffer;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// 检查Application状态,只有在WiFi配置模式下才处理音频
|
||||
if (app->GetDeviceState() != kDeviceStateWifiConfiguring) {
|
||||
// 不在WiFi配置状态,休眠100ms后再检查
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!app->GetAudioService().ReadAudioData(audio_data, 16000, 480)) { // 16kHz, 480 samples corresponds to 30ms data
|
||||
// 读取音频失败,短暂延迟后重试
|
||||
ESP_LOGI(kLogTag, "Failed to read audio data, retrying.");
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (input_channels == 2) { // 如果是双声道输入,转换为单声道
|
||||
auto mono_data = std::vector<int16_t>(audio_data.size() / 2);
|
||||
for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) {
|
||||
mono_data[i] = audio_data[j];
|
||||
}
|
||||
audio_data = std::move(mono_data);
|
||||
}
|
||||
|
||||
// Downsample the audio data
|
||||
std::vector<float> downsampled_data;
|
||||
size_t last_index = 0;
|
||||
|
||||
if (kDownsampleStep > 1.0f) {
|
||||
downsampled_data.reserve(audio_data.size() / static_cast<size_t>(kDownsampleStep));
|
||||
for (size_t i = 0; i < audio_data.size(); ++i) {
|
||||
size_t sample_index = static_cast<size_t>(i / kDownsampleStep);
|
||||
if ((sample_index + 1) > last_index) {
|
||||
downsampled_data.push_back(static_cast<float>(audio_data[i]));
|
||||
last_index = sample_index + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
downsampled_data.reserve(audio_data.size());
|
||||
for (int16_t sample : audio_data) {
|
||||
downsampled_data.push_back(static_cast<float>(sample));
|
||||
}
|
||||
}
|
||||
|
||||
// Process audio samples to get probability data
|
||||
auto probabilities = signal_processor.ProcessAudioSamples(downsampled_data);
|
||||
|
||||
// Feed probability data to the data buffer
|
||||
if (data_buffer.ProcessProbabilityData(probabilities, 0.5f)) {
|
||||
// If complete data was received, extract WiFi credentials
|
||||
if (data_buffer.decoded_text.has_value()) {
|
||||
ESP_LOGI(kLogTag, "Received text data: %s", data_buffer.decoded_text->c_str());
|
||||
display->SetChatMessage("system", data_buffer.decoded_text->c_str());
|
||||
|
||||
// Split SSID and password by newline character
|
||||
std::string wifi_ssid, wifi_password;
|
||||
size_t newline_position = data_buffer.decoded_text->find('\n');
|
||||
if (newline_position != std::string::npos) {
|
||||
wifi_ssid = data_buffer.decoded_text->substr(0, newline_position);
|
||||
wifi_password = data_buffer.decoded_text->substr(newline_position + 1);
|
||||
ESP_LOGI(kLogTag, "WiFi SSID: %s, Password: %s", wifi_ssid.c_str(), wifi_password.c_str());
|
||||
} else {
|
||||
ESP_LOGE(kLogTag, "Invalid data format, no newline character found");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wifi_ap->ConnectToWifi(wifi_ssid, wifi_password)) {
|
||||
wifi_ap->Save(wifi_ssid, wifi_password); // Save WiFi credentials
|
||||
esp_restart(); // Restart device to apply new WiFi configuration
|
||||
} else {
|
||||
ESP_LOGE(kLogTag, "Failed to connect to WiFi with received credentials");
|
||||
}
|
||||
data_buffer.decoded_text.reset(); // Clear processed data
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms delay
|
||||
}
|
||||
}
|
||||
|
||||
// Default start and end transmission identifiers
|
||||
// \x01\x02 = 00000001 00000010
|
||||
const std::vector<uint8_t> kDefaultStartTransmissionPattern = {
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0};
|
||||
|
||||
// \x03\x04 = 00000011 00000100
|
||||
const std::vector<uint8_t> kDefaultEndTransmissionPattern = {
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0};
|
||||
|
||||
// FrequencyDetector implementation
|
||||
FrequencyDetector::FrequencyDetector(float frequency, size_t window_size)
|
||||
: frequency_(frequency), window_size_(window_size) {
|
||||
frequency_bin_ = std::floor(frequency_ * static_cast<float>(window_size_));
|
||||
angular_frequency_ = 2.0f * M_PI * frequency_;
|
||||
cos_coefficient_ = std::cos(angular_frequency_);
|
||||
sin_coefficient_ = std::sin(angular_frequency_);
|
||||
filter_coefficient_ = 2.0f * cos_coefficient_;
|
||||
|
||||
// Initialize state buffer
|
||||
state_buffer_.push_back(0.0f);
|
||||
state_buffer_.push_back(0.0f);
|
||||
}
|
||||
|
||||
void FrequencyDetector::Reset() {
|
||||
state_buffer_.clear();
|
||||
state_buffer_.push_back(0.0f);
|
||||
state_buffer_.push_back(0.0f);
|
||||
}
|
||||
|
||||
void FrequencyDetector::ProcessSample(float sample) {
|
||||
if (state_buffer_.size() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
float s_minus_2 = state_buffer_.front(); // S[-2]
|
||||
state_buffer_.pop_front();
|
||||
float s_minus_1 = state_buffer_.front(); // S[-1]
|
||||
state_buffer_.pop_front();
|
||||
|
||||
float s_current = sample + filter_coefficient_ * s_minus_1 - s_minus_2;
|
||||
|
||||
state_buffer_.push_back(s_minus_1); // Put S[-1] back
|
||||
state_buffer_.push_back(s_current); // Add new S[0]
|
||||
}
|
||||
|
||||
float FrequencyDetector::GetAmplitude() const {
|
||||
if (state_buffer_.size() < 2) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float s_minus_1 = state_buffer_[1]; // S[-1]
|
||||
float s_minus_2 = state_buffer_[0]; // S[-2]
|
||||
float real_part = cos_coefficient_ * s_minus_1 - s_minus_2; // Real part
|
||||
float imaginary_part = sin_coefficient_ * s_minus_1; // Imaginary part
|
||||
|
||||
return std::sqrt(real_part * real_part + imaginary_part * imaginary_part) /
|
||||
(static_cast<float>(window_size_) / 2.0f);
|
||||
}
|
||||
|
||||
// AudioSignalProcessor implementation
|
||||
AudioSignalProcessor::AudioSignalProcessor(size_t sample_rate, size_t mark_frequency, size_t space_frequency,
|
||||
size_t bit_rate, size_t window_size)
|
||||
: input_buffer_size_(window_size), output_sample_count_(0) {
|
||||
if (sample_rate % bit_rate != 0) {
|
||||
// On ESP32 we can continue execution, but log the error
|
||||
ESP_LOGW(kLogTag, "Sample rate %zu is not divisible by bit rate %zu", sample_rate, bit_rate);
|
||||
}
|
||||
|
||||
float normalized_mark_freq = static_cast<float>(mark_frequency) / static_cast<float>(sample_rate);
|
||||
float normalized_space_freq = static_cast<float>(space_frequency) / static_cast<float>(sample_rate);
|
||||
|
||||
mark_detector_ = std::make_unique<FrequencyDetector>(normalized_mark_freq, window_size);
|
||||
space_detector_ = std::make_unique<FrequencyDetector>(normalized_space_freq, window_size);
|
||||
|
||||
samples_per_bit_ = sample_rate / bit_rate; // Number of samples per bit
|
||||
}
|
||||
|
||||
std::vector<float> AudioSignalProcessor::ProcessAudioSamples(const std::vector<float> &samples) {
|
||||
std::vector<float> result;
|
||||
|
||||
for (float sample : samples) {
|
||||
if (input_buffer_.size() < input_buffer_size_) {
|
||||
input_buffer_.push_back(sample); // Just add, don't process yet
|
||||
} else {
|
||||
// Input buffer is full, process the data
|
||||
input_buffer_.pop_front(); // Remove oldest sample
|
||||
input_buffer_.push_back(sample); // Add new sample
|
||||
output_sample_count_++;
|
||||
|
||||
if (output_sample_count_ >= samples_per_bit_) {
|
||||
// Process all samples in the window using Goertzel algorithm
|
||||
for (float window_sample : input_buffer_) {
|
||||
mark_detector_->ProcessSample(window_sample);
|
||||
space_detector_->ProcessSample(window_sample);
|
||||
}
|
||||
|
||||
float mark_amplitude = mark_detector_->GetAmplitude(); // Mark amplitude
|
||||
float space_amplitude = space_detector_->GetAmplitude(); // Space amplitude
|
||||
|
||||
// Avoid division by zero
|
||||
float mark_probability = mark_amplitude /
|
||||
(space_amplitude + mark_amplitude + std::numeric_limits<float>::epsilon());
|
||||
result.push_back(mark_probability);
|
||||
|
||||
// Reset detector windows
|
||||
mark_detector_->Reset();
|
||||
space_detector_->Reset();
|
||||
output_sample_count_ = 0; // Reset output counter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// AudioDataBuffer implementation
|
||||
AudioDataBuffer::AudioDataBuffer()
|
||||
: current_state_(DataReceptionState::kInactive),
|
||||
start_of_transmission_(kDefaultStartTransmissionPattern),
|
||||
end_of_transmission_(kDefaultEndTransmissionPattern),
|
||||
enable_checksum_validation_(true) {
|
||||
identifier_buffer_size_ = std::max(start_of_transmission_.size(), end_of_transmission_.size());
|
||||
max_bit_buffer_size_ = 776; // Preset bit buffer size, 776 bits = (32 + 1 + 63 + 1) * 8 = 776
|
||||
|
||||
bit_buffer_.reserve(max_bit_buffer_size_);
|
||||
}
|
||||
|
||||
AudioDataBuffer::AudioDataBuffer(size_t max_byte_size, const std::vector<uint8_t> &start_identifier,
|
||||
const std::vector<uint8_t> &end_identifier, bool enable_checksum)
|
||||
: current_state_(DataReceptionState::kInactive),
|
||||
start_of_transmission_(start_identifier),
|
||||
end_of_transmission_(end_identifier),
|
||||
enable_checksum_validation_(enable_checksum) {
|
||||
identifier_buffer_size_ = std::max(start_of_transmission_.size(), end_of_transmission_.size());
|
||||
max_bit_buffer_size_ = max_byte_size * 8; // Bit buffer size in bytes
|
||||
|
||||
bit_buffer_.reserve(max_bit_buffer_size_);
|
||||
}
|
||||
|
||||
uint8_t AudioDataBuffer::CalculateChecksum(const std::string &text) {
|
||||
uint8_t checksum = 0;
|
||||
for (char character : text) {
|
||||
checksum += static_cast<uint8_t>(character);
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
|
||||
void AudioDataBuffer::ClearBuffers() {
|
||||
identifier_buffer_.clear();
|
||||
bit_buffer_.clear();
|
||||
}
|
||||
|
||||
bool AudioDataBuffer::ProcessProbabilityData(const std::vector<float> &probabilities, float threshold) {
|
||||
for (float probability : probabilities) {
|
||||
uint8_t bit = (probability > threshold) ? 1 : 0;
|
||||
|
||||
if (identifier_buffer_.size() >= identifier_buffer_size_) {
|
||||
identifier_buffer_.pop_front(); // Maintain buffer size
|
||||
}
|
||||
identifier_buffer_.push_back(bit);
|
||||
|
||||
// Process received bit based on state machine
|
||||
switch (current_state_) {
|
||||
case DataReceptionState::kInactive:
|
||||
if (identifier_buffer_.size() >= start_of_transmission_.size()) {
|
||||
current_state_ = DataReceptionState::kWaiting; // Enter waiting state
|
||||
ESP_LOGI(kLogTag, "Entering Waiting state");
|
||||
}
|
||||
break;
|
||||
|
||||
case DataReceptionState::kWaiting:
|
||||
// Waiting state, possibly waiting for transmission end
|
||||
if (identifier_buffer_.size() >= start_of_transmission_.size()) {
|
||||
std::vector<uint8_t> identifier_snapshot(identifier_buffer_.begin(), identifier_buffer_.end());
|
||||
if (identifier_snapshot == start_of_transmission_)
|
||||
{
|
||||
ClearBuffers(); // Clear buffers
|
||||
current_state_ = DataReceptionState::kReceiving; // Enter receiving state
|
||||
ESP_LOGI(kLogTag, "Entering Receiving state");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case DataReceptionState::kReceiving:
|
||||
bit_buffer_.push_back(bit);
|
||||
if (identifier_buffer_.size() >= end_of_transmission_.size()) {
|
||||
std::vector<uint8_t> identifier_snapshot(identifier_buffer_.begin(), identifier_buffer_.end());
|
||||
if (identifier_snapshot == end_of_transmission_) {
|
||||
current_state_ = DataReceptionState::kInactive; // Enter inactive state
|
||||
|
||||
// Convert bits to bytes
|
||||
std::vector<uint8_t> bytes = ConvertBitsToBytes(bit_buffer_);
|
||||
|
||||
uint8_t received_checksum = 0;
|
||||
size_t minimum_length = 0;
|
||||
|
||||
if (enable_checksum_validation_) {
|
||||
// If checksum is required, last byte is checksum
|
||||
minimum_length = 1 + start_of_transmission_.size() / 8;
|
||||
if (bytes.size() >= minimum_length)
|
||||
{
|
||||
received_checksum = bytes[bytes.size() - start_of_transmission_.size() / 8 - 1];
|
||||
}
|
||||
} else {
|
||||
minimum_length = start_of_transmission_.size() / 8;
|
||||
}
|
||||
|
||||
if (bytes.size() < minimum_length) {
|
||||
ClearBuffers();
|
||||
ESP_LOGW(kLogTag, "Data too short, clearing buffer");
|
||||
return false; // Data too short, return failure
|
||||
}
|
||||
|
||||
// Extract text data (remove trailing identifier part)
|
||||
std::vector<uint8_t> text_bytes(
|
||||
bytes.begin(), bytes.begin() + bytes.size() - minimum_length);
|
||||
|
||||
std::string result(text_bytes.begin(), text_bytes.end());
|
||||
|
||||
// Validate checksum if required
|
||||
if (enable_checksum_validation_) {
|
||||
uint8_t calculated_checksum = CalculateChecksum(result);
|
||||
if (calculated_checksum != received_checksum) {
|
||||
// Checksum mismatch
|
||||
ESP_LOGW(kLogTag, "Checksum mismatch: expected %d, got %d",
|
||||
received_checksum, calculated_checksum);
|
||||
ClearBuffers();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ClearBuffers();
|
||||
decoded_text = result;
|
||||
return true; // Return success
|
||||
} else if (bit_buffer_.size() >= max_bit_buffer_size_) {
|
||||
// If not end identifier and bit buffer is full, reset
|
||||
ClearBuffers();
|
||||
ESP_LOGW(kLogTag, "Buffer overflow, clearing buffer");
|
||||
current_state_ = DataReceptionState::kInactive; // Reset state machine
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AudioDataBuffer::ConvertBitsToBytes(const std::vector<uint8_t> &bits) const {
|
||||
std::vector<uint8_t> bytes;
|
||||
|
||||
// Ensure number of bits is a multiple of 8
|
||||
size_t complete_bytes_count = bits.size() / 8;
|
||||
bytes.reserve(complete_bytes_count);
|
||||
|
||||
for (size_t i = 0; i < complete_bytes_count; ++i) {
|
||||
uint8_t byte_value = 0;
|
||||
for (size_t j = 0; j < 8; ++j) {
|
||||
byte_value |= bits[i * 8 + j] << (7 - j);
|
||||
}
|
||||
bytes.push_back(byte_value);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
177
main/boards/common/afsk_demod.h
Normal file
177
main/boards/common/afsk_demod.h
Normal file
@@ -0,0 +1,177 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <cmath>
|
||||
#include "wifi_configuration_ap.h"
|
||||
#include "application.h"
|
||||
|
||||
// Audio signal processing constants for WiFi configuration via audio
|
||||
const size_t kAudioSampleRate = 6400;
|
||||
const size_t kMarkFrequency = 1800;
|
||||
const size_t kSpaceFrequency = 1500;
|
||||
const size_t kBitRate = 100;
|
||||
const size_t kWindowSize = 64;
|
||||
|
||||
namespace audio_wifi_config
|
||||
{
|
||||
// Main function to receive WiFi credentials through audio signal
|
||||
void ReceiveWifiCredentialsFromAudio(Application *app, WifiConfigurationAp *wifi_ap, Display *display,
|
||||
size_t input_channels = 1);
|
||||
|
||||
/**
|
||||
* Goertzel algorithm implementation for single frequency detection
|
||||
* Used to detect specific audio frequencies in the AFSK demodulation process
|
||||
*/
|
||||
class FrequencyDetector
|
||||
{
|
||||
private:
|
||||
float frequency_; // Target frequency (normalized, i.e., f / fs)
|
||||
size_t window_size_; // Window size for analysis
|
||||
float frequency_bin_; // Frequency bin
|
||||
float angular_frequency_; // Angular frequency
|
||||
float cos_coefficient_; // cos(w)
|
||||
float sin_coefficient_; // sin(w)
|
||||
float filter_coefficient_; // 2 * cos(w)
|
||||
std::deque<float> state_buffer_; // Circular buffer for storing S[-1] and S[-2]
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor
|
||||
* @param frequency Normalized frequency (f / fs)
|
||||
* @param window_size Window size for analysis
|
||||
*/
|
||||
FrequencyDetector(float frequency, size_t window_size);
|
||||
|
||||
/**
|
||||
* Reset the detector state
|
||||
*/
|
||||
void Reset();
|
||||
|
||||
/**
|
||||
* Process one audio sample
|
||||
* @param sample Input audio sample
|
||||
*/
|
||||
void ProcessSample(float sample);
|
||||
|
||||
/**
|
||||
* Calculate current amplitude
|
||||
* @return Amplitude value
|
||||
*/
|
||||
float GetAmplitude() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Audio signal processor for Mark/Space frequency pair detection
|
||||
* Processes audio signals to extract digital data using AFSK demodulation
|
||||
*/
|
||||
class AudioSignalProcessor
|
||||
{
|
||||
private:
|
||||
std::deque<float> input_buffer_; // Input sample buffer
|
||||
size_t input_buffer_size_; // Input buffer size = window size
|
||||
size_t output_sample_count_; // Output sample counter
|
||||
size_t samples_per_bit_; // Samples per bit threshold
|
||||
std::unique_ptr<FrequencyDetector> mark_detector_; // Mark frequency detector
|
||||
std::unique_ptr<FrequencyDetector> space_detector_; // Space frequency detector
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor
|
||||
* @param sample_rate Audio sampling rate
|
||||
* @param mark_frequency Mark frequency for digital '1'
|
||||
* @param space_frequency Space frequency for digital '0'
|
||||
* @param bit_rate Data transmission bit rate
|
||||
* @param window_size Analysis window size
|
||||
*/
|
||||
AudioSignalProcessor(size_t sample_rate, size_t mark_frequency, size_t space_frequency,
|
||||
size_t bit_rate, size_t window_size);
|
||||
|
||||
/**
|
||||
* Process input audio samples
|
||||
* @param samples Input audio sample vector
|
||||
* @return Vector of Mark probability values (0.0 to 1.0)
|
||||
*/
|
||||
std::vector<float> ProcessAudioSamples(const std::vector<float> &samples);
|
||||
};
|
||||
|
||||
/**
|
||||
* Data reception state machine states
|
||||
*/
|
||||
enum class DataReceptionState
|
||||
{
|
||||
kInactive, // Waiting for start signal
|
||||
kWaiting, // Detected potential start, waiting for confirmation
|
||||
kReceiving // Actively receiving data
|
||||
};
|
||||
|
||||
/**
|
||||
* Data buffer for managing audio-to-digital data conversion
|
||||
* Handles the complete process from audio signal to decoded text data
|
||||
*/
|
||||
class AudioDataBuffer
|
||||
{
|
||||
private:
|
||||
DataReceptionState current_state_; // Current reception state
|
||||
std::deque<uint8_t> identifier_buffer_; // Buffer for start/end identifier detection
|
||||
size_t identifier_buffer_size_; // Identifier buffer size
|
||||
std::vector<uint8_t> bit_buffer_; // Buffer for storing bit stream
|
||||
size_t max_bit_buffer_size_; // Maximum bit buffer size
|
||||
const std::vector<uint8_t> start_of_transmission_; // Start-of-transmission identifier
|
||||
const std::vector<uint8_t> end_of_transmission_; // End-of-transmission identifier
|
||||
bool enable_checksum_validation_; // Whether to validate checksum
|
||||
|
||||
public:
|
||||
std::optional<std::string> decoded_text; // Successfully decoded text data
|
||||
|
||||
/**
|
||||
* Default constructor using predefined start and end identifiers
|
||||
*/
|
||||
AudioDataBuffer();
|
||||
|
||||
/**
|
||||
* Constructor with custom parameters
|
||||
* @param max_byte_size Expected maximum data size in bytes
|
||||
* @param start_identifier Start-of-transmission identifier
|
||||
* @param end_identifier End-of-transmission identifier
|
||||
* @param enable_checksum Whether to enable checksum validation
|
||||
*/
|
||||
AudioDataBuffer(size_t max_byte_size, const std::vector<uint8_t> &start_identifier,
|
||||
const std::vector<uint8_t> &end_identifier, bool enable_checksum = false);
|
||||
|
||||
/**
|
||||
* Process probability data and attempt to decode
|
||||
* @param probabilities Vector of Mark probabilities
|
||||
* @param threshold Decision threshold for bit detection
|
||||
* @return true if complete data was successfully received and decoded
|
||||
*/
|
||||
bool ProcessProbabilityData(const std::vector<float> &probabilities, float threshold = 0.5f);
|
||||
|
||||
/**
|
||||
* Calculate checksum for ASCII text
|
||||
* @param text Input text string
|
||||
* @return Checksum value (0-255)
|
||||
*/
|
||||
static uint8_t CalculateChecksum(const std::string &text);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Convert bit vector to byte vector
|
||||
* @param bits Input bit vector
|
||||
* @return Converted byte vector
|
||||
*/
|
||||
std::vector<uint8_t> ConvertBitsToBytes(const std::vector<uint8_t> &bits) const;
|
||||
|
||||
/**
|
||||
* Clear all buffers and reset state
|
||||
*/
|
||||
void ClearBuffers();
|
||||
};
|
||||
|
||||
// Default start and end transmission identifiers
|
||||
extern const std::vector<uint8_t> kDefaultStartTransmissionPattern;
|
||||
extern const std::vector<uint8_t> kDefaultEndTransmissionPattern;
|
||||
}
|
||||
41
main/boards/common/axp2101.cc
Normal file
41
main/boards/common/axp2101.cc
Normal file
@@ -0,0 +1,41 @@
|
||||
#include "axp2101.h"
|
||||
#include "board.h"
|
||||
#include "display.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
|
||||
#define TAG "Axp2101"
|
||||
|
||||
Axp2101::Axp2101(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
|
||||
}
|
||||
|
||||
int Axp2101::GetBatteryCurrentDirection() {
|
||||
return (ReadReg(0x01) & 0b01100000) >> 5;
|
||||
}
|
||||
|
||||
bool Axp2101::IsCharging() {
|
||||
return GetBatteryCurrentDirection() == 1;
|
||||
}
|
||||
|
||||
bool Axp2101::IsDischarging() {
|
||||
return GetBatteryCurrentDirection() == 2;
|
||||
}
|
||||
|
||||
bool Axp2101::IsChargingDone() {
|
||||
uint8_t value = ReadReg(0x01);
|
||||
return (value & 0b00000111) == 0b00000100;
|
||||
}
|
||||
|
||||
int Axp2101::GetBatteryLevel() {
|
||||
return ReadReg(0xA4);
|
||||
}
|
||||
|
||||
float Axp2101::GetTemperature() {
|
||||
return ReadReg(0xA5);
|
||||
}
|
||||
|
||||
void Axp2101::PowerOff() {
|
||||
uint8_t value = ReadReg(0x10);
|
||||
value = value | 0x01;
|
||||
WriteReg(0x10, value);
|
||||
}
|
||||
20
main/boards/common/axp2101.h
Normal file
20
main/boards/common/axp2101.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef __AXP2101_H__
|
||||
#define __AXP2101_H__
|
||||
|
||||
#include "i2c_device.h"
|
||||
|
||||
class Axp2101 : public I2cDevice {
|
||||
public:
|
||||
Axp2101(i2c_master_bus_handle_t i2c_bus, uint8_t addr);
|
||||
bool IsCharging();
|
||||
bool IsDischarging();
|
||||
bool IsChargingDone();
|
||||
int GetBatteryLevel();
|
||||
float GetTemperature();
|
||||
void PowerOff();
|
||||
|
||||
private:
|
||||
int GetBatteryCurrentDirection();
|
||||
};
|
||||
|
||||
#endif
|
||||
121
main/boards/common/backlight.cc
Normal file
121
main/boards/common/backlight.cc
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "backlight.h"
|
||||
#include "settings.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <driver/ledc.h>
|
||||
|
||||
#define TAG "Backlight"
|
||||
|
||||
|
||||
Backlight::Backlight() {
|
||||
// 创建背光渐变定时器
|
||||
const esp_timer_create_args_t timer_args = {
|
||||
.callback = [](void* arg) {
|
||||
auto self = static_cast<Backlight*>(arg);
|
||||
self->OnTransitionTimer();
|
||||
},
|
||||
.arg = this,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "backlight_timer",
|
||||
.skip_unhandled_events = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &transition_timer_));
|
||||
}
|
||||
|
||||
Backlight::~Backlight() {
|
||||
if (transition_timer_ != nullptr) {
|
||||
esp_timer_stop(transition_timer_);
|
||||
esp_timer_delete(transition_timer_);
|
||||
}
|
||||
}
|
||||
|
||||
void Backlight::RestoreBrightness() {
|
||||
// Load brightness from settings
|
||||
Settings settings("display");
|
||||
int saved_brightness = settings.GetInt("brightness", 75);
|
||||
|
||||
// 检查亮度值是否为0或过小,设置默认值
|
||||
if (saved_brightness <= 0) {
|
||||
ESP_LOGW(TAG, "Brightness value (%d) is too small, setting to default (10)", saved_brightness);
|
||||
saved_brightness = 10; // 设置一个较低的默认值
|
||||
}
|
||||
|
||||
SetBrightness(saved_brightness);
|
||||
}
|
||||
|
||||
void Backlight::SetBrightness(uint8_t brightness, bool permanent) {
|
||||
if (brightness > 100) {
|
||||
brightness = 100;
|
||||
}
|
||||
|
||||
if (brightness_ == brightness) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (permanent) {
|
||||
Settings settings("display", true);
|
||||
settings.SetInt("brightness", brightness);
|
||||
}
|
||||
|
||||
target_brightness_ = brightness;
|
||||
step_ = (target_brightness_ > brightness_) ? 1 : -1;
|
||||
|
||||
if (transition_timer_ != nullptr) {
|
||||
// 启动定时器,每 5ms 更新一次
|
||||
esp_timer_start_periodic(transition_timer_, 5 * 1000);
|
||||
}
|
||||
ESP_LOGI(TAG, "Set brightness to %d", brightness);
|
||||
}
|
||||
|
||||
void Backlight::OnTransitionTimer() {
|
||||
if (brightness_ == target_brightness_) {
|
||||
esp_timer_stop(transition_timer_);
|
||||
return;
|
||||
}
|
||||
|
||||
brightness_ += step_;
|
||||
SetBrightnessImpl(brightness_);
|
||||
|
||||
if (brightness_ == target_brightness_) {
|
||||
esp_timer_stop(transition_timer_);
|
||||
}
|
||||
}
|
||||
|
||||
PwmBacklight::PwmBacklight(gpio_num_t pin, bool output_invert, uint32_t freq_hz) : Backlight() {
|
||||
const ledc_timer_config_t backlight_timer = {
|
||||
.speed_mode = LEDC_LOW_SPEED_MODE,
|
||||
.duty_resolution = LEDC_TIMER_10_BIT,
|
||||
.timer_num = LEDC_TIMER_0,
|
||||
.freq_hz = freq_hz, //背光pwm频率需要高一点,防止电感啸叫
|
||||
.clk_cfg = LEDC_AUTO_CLK,
|
||||
.deconfigure = false
|
||||
};
|
||||
ESP_ERROR_CHECK(ledc_timer_config(&backlight_timer));
|
||||
|
||||
// Setup LEDC peripheral for PWM backlight control
|
||||
const ledc_channel_config_t backlight_channel = {
|
||||
.gpio_num = pin,
|
||||
.speed_mode = LEDC_LOW_SPEED_MODE,
|
||||
.channel = LEDC_CHANNEL_0,
|
||||
.intr_type = LEDC_INTR_DISABLE,
|
||||
.timer_sel = LEDC_TIMER_0,
|
||||
.duty = 0,
|
||||
.hpoint = 0,
|
||||
.flags = {
|
||||
.output_invert = output_invert,
|
||||
}
|
||||
};
|
||||
ESP_ERROR_CHECK(ledc_channel_config(&backlight_channel));
|
||||
}
|
||||
|
||||
PwmBacklight::~PwmBacklight() {
|
||||
ledc_stop(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0);
|
||||
}
|
||||
|
||||
void PwmBacklight::SetBrightnessImpl(uint8_t brightness) {
|
||||
// LEDC resolution set to 10bits, thus: 100% = 1023
|
||||
uint32_t duty_cycle = (1023 * brightness) / 100;
|
||||
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty_cycle);
|
||||
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
|
||||
}
|
||||
|
||||
36
main/boards/common/backlight.h
Normal file
36
main/boards/common/backlight.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_timer.h>
|
||||
|
||||
|
||||
class Backlight {
|
||||
public:
|
||||
Backlight();
|
||||
~Backlight();
|
||||
|
||||
void RestoreBrightness();
|
||||
void SetBrightness(uint8_t brightness, bool permanent = false);
|
||||
inline uint8_t brightness() const { return brightness_; }
|
||||
|
||||
protected:
|
||||
void OnTransitionTimer();
|
||||
virtual void SetBrightnessImpl(uint8_t brightness) = 0;
|
||||
|
||||
esp_timer_handle_t transition_timer_ = nullptr;
|
||||
uint8_t brightness_ = 0;
|
||||
uint8_t target_brightness_ = 0;
|
||||
uint8_t step_ = 1;
|
||||
};
|
||||
|
||||
|
||||
class PwmBacklight : public Backlight {
|
||||
public:
|
||||
PwmBacklight(gpio_num_t pin, bool output_invert = false, uint32_t freq_hz = 25000);
|
||||
~PwmBacklight();
|
||||
|
||||
void SetBrightnessImpl(uint8_t brightness) override;
|
||||
};
|
||||
181
main/boards/common/board.cc
Normal file
181
main/boards/common/board.cc
Normal file
@@ -0,0 +1,181 @@
|
||||
#include "board.h"
|
||||
#include "system_info.h"
|
||||
#include "settings.h"
|
||||
#include "display/display.h"
|
||||
#include "assets/lang_config.h"
|
||||
#include "esp32_music.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_chip_info.h>
|
||||
#include <esp_random.h>
|
||||
|
||||
#define TAG "Board"
|
||||
|
||||
Board::Board() {
|
||||
music_ = nullptr; // 先初始化为空指针
|
||||
|
||||
Settings settings("board", true);
|
||||
uuid_ = settings.GetString("uuid");
|
||||
if (uuid_.empty()) {
|
||||
uuid_ = GenerateUuid();
|
||||
settings.SetString("uuid", uuid_);
|
||||
}
|
||||
ESP_LOGI(TAG, "UUID=%s SKU=%s", uuid_.c_str(), BOARD_NAME);
|
||||
|
||||
// 初始化音乐播放器
|
||||
music_ = new Esp32Music();
|
||||
ESP_LOGI(TAG, "Music player initialized for all boards");
|
||||
}
|
||||
|
||||
Board::~Board() {
|
||||
if (music_) {
|
||||
delete music_;
|
||||
music_ = nullptr;
|
||||
ESP_LOGI(TAG, "Music player destroyed");
|
||||
}
|
||||
}
|
||||
|
||||
std::string Board::GenerateUuid() {
|
||||
// UUID v4 需要 16 字节的随机数据
|
||||
uint8_t uuid[16];
|
||||
|
||||
// 使用 ESP32 的硬件随机数生成器
|
||||
esp_fill_random(uuid, sizeof(uuid));
|
||||
|
||||
// 设置版本 (版本 4) 和变体位
|
||||
uuid[6] = (uuid[6] & 0x0F) | 0x40; // 版本 4
|
||||
uuid[8] = (uuid[8] & 0x3F) | 0x80; // 变体 1
|
||||
|
||||
// 将字节转换为标准的 UUID 字符串格式
|
||||
char uuid_str[37];
|
||||
snprintf(uuid_str, sizeof(uuid_str),
|
||||
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
||||
uuid[0], uuid[1], uuid[2], uuid[3],
|
||||
uuid[4], uuid[5], uuid[6], uuid[7],
|
||||
uuid[8], uuid[9], uuid[10], uuid[11],
|
||||
uuid[12], uuid[13], uuid[14], uuid[15]);
|
||||
|
||||
return std::string(uuid_str);
|
||||
}
|
||||
|
||||
bool Board::GetBatteryLevel(int &level, bool& charging, bool& discharging) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Board::GetTemperature(float& esp32temp){
|
||||
return false;
|
||||
}
|
||||
|
||||
Display* Board::GetDisplay() {
|
||||
static NoDisplay display;
|
||||
return &display;
|
||||
}
|
||||
|
||||
Camera* Board::GetCamera() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Music* Board::GetMusic() {
|
||||
return music_;
|
||||
}
|
||||
|
||||
Led* Board::GetLed() {
|
||||
static NoLed led;
|
||||
return &led;
|
||||
}
|
||||
|
||||
std::string Board::GetJson() {
|
||||
/*
|
||||
{
|
||||
"version": 2,
|
||||
"flash_size": 4194304,
|
||||
"psram_size": 0,
|
||||
"minimum_free_heap_size": 123456,
|
||||
"mac_address": "00:00:00:00:00:00",
|
||||
"uuid": "00000000-0000-0000-0000-000000000000",
|
||||
"chip_model_name": "esp32s3",
|
||||
"chip_info": {
|
||||
"model": 1,
|
||||
"cores": 2,
|
||||
"revision": 0,
|
||||
"features": 0
|
||||
},
|
||||
"application": {
|
||||
"name": "my-app",
|
||||
"version": "1.0.0",
|
||||
"compile_time": "2021-01-01T00:00:00Z"
|
||||
"idf_version": "4.2-dev"
|
||||
"elf_sha256": ""
|
||||
},
|
||||
"partition_table": [
|
||||
"app": {
|
||||
"label": "app",
|
||||
"type": 1,
|
||||
"subtype": 2,
|
||||
"address": 0x10000,
|
||||
"size": 0x100000
|
||||
}
|
||||
],
|
||||
"ota": {
|
||||
"label": "ota_0"
|
||||
},
|
||||
"board": {
|
||||
...
|
||||
}
|
||||
}
|
||||
*/
|
||||
std::string json = R"({"version":2,"language":")" + std::string(Lang::CODE) + R"(",)";
|
||||
json += R"("flash_size":)" + std::to_string(SystemInfo::GetFlashSize()) + R"(,)";
|
||||
json += R"("minimum_free_heap_size":")" + std::to_string(SystemInfo::GetMinimumFreeHeapSize()) + R"(",)";
|
||||
json += R"("mac_address":")" + SystemInfo::GetMacAddress() + R"(",)";
|
||||
json += R"("uuid":")" + uuid_ + R"(",)";
|
||||
json += R"("chip_model_name":")" + SystemInfo::GetChipModelName() + R"(",)";
|
||||
|
||||
esp_chip_info_t chip_info;
|
||||
esp_chip_info(&chip_info);
|
||||
json += R"("chip_info":{)";
|
||||
json += R"("model":)" + std::to_string(chip_info.model) + R"(,)";
|
||||
json += R"("cores":)" + std::to_string(chip_info.cores) + R"(,)";
|
||||
json += R"("revision":)" + std::to_string(chip_info.revision) + R"(,)";
|
||||
json += R"("features":)" + std::to_string(chip_info.features) + R"(},)";
|
||||
|
||||
auto app_desc = esp_app_get_description();
|
||||
json += R"("application":{)";
|
||||
json += R"("name":")" + std::string(app_desc->project_name) + R"(",)";
|
||||
json += R"("version":")" + std::string(app_desc->version) + R"(",)";
|
||||
json += R"("compile_time":")" + std::string(app_desc->date) + R"(T)" + std::string(app_desc->time) + R"(Z",)";
|
||||
json += R"("idf_version":")" + std::string(app_desc->idf_ver) + R"(",)";
|
||||
char sha256_str[65];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
snprintf(sha256_str + i * 2, sizeof(sha256_str) - i * 2, "%02x", app_desc->app_elf_sha256[i]);
|
||||
}
|
||||
json += R"("elf_sha256":")" + std::string(sha256_str) + R"(")";
|
||||
json += R"(},)";
|
||||
|
||||
json += R"("partition_table": [)";
|
||||
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
|
||||
while (it) {
|
||||
const esp_partition_t *partition = esp_partition_get(it);
|
||||
json += R"({)";
|
||||
json += R"("label":")" + std::string(partition->label) + R"(",)";
|
||||
json += R"("type":)" + std::to_string(partition->type) + R"(,)";
|
||||
json += R"("subtype":)" + std::to_string(partition->subtype) + R"(,)";
|
||||
json += R"("address":)" + std::to_string(partition->address) + R"(,)";
|
||||
json += R"("size":)" + std::to_string(partition->size) + R"(},)";;
|
||||
it = esp_partition_next(it);
|
||||
}
|
||||
json.pop_back(); // Remove the last comma
|
||||
json += R"(],)";
|
||||
|
||||
json += R"("ota":{)";
|
||||
auto ota_partition = esp_ota_get_running_partition();
|
||||
json += R"("label":")" + std::string(ota_partition->label) + R"(")";
|
||||
json += R"(},)";
|
||||
|
||||
json += R"("board":)" + GetBoardJson();
|
||||
|
||||
// Close the JSON object
|
||||
json += R"(})";
|
||||
return json;
|
||||
}
|
||||
65
main/boards/common/board.h
Normal file
65
main/boards/common/board.h
Normal file
@@ -0,0 +1,65 @@
|
||||
#ifndef BOARD_H
|
||||
#define BOARD_H
|
||||
|
||||
#include <http.h>
|
||||
#include <web_socket.h>
|
||||
#include <mqtt.h>
|
||||
#include <udp.h>
|
||||
#include <string>
|
||||
#include <network_interface.h>
|
||||
|
||||
#include "led/led.h"
|
||||
#include "backlight.h"
|
||||
#include "camera.h"
|
||||
#include "music.h"
|
||||
|
||||
void* create_board();
|
||||
class AudioCodec;
|
||||
class Display;
|
||||
class Board {
|
||||
private:
|
||||
Board(const Board&) = delete; // 禁用拷贝构造函数
|
||||
Board& operator=(const Board&) = delete; // 禁用赋值操作
|
||||
|
||||
protected:
|
||||
Board();
|
||||
std::string GenerateUuid();
|
||||
|
||||
// 软件生成的设备唯一标识
|
||||
std::string uuid_;
|
||||
|
||||
// 音乐播放器实例
|
||||
Music* music_;
|
||||
|
||||
public:
|
||||
static Board& GetInstance() {
|
||||
static Board* instance = static_cast<Board*>(create_board());
|
||||
return *instance;
|
||||
}
|
||||
|
||||
virtual ~Board();
|
||||
virtual std::string GetBoardType() = 0;
|
||||
virtual std::string GetUuid() { return uuid_; }
|
||||
virtual Backlight* GetBacklight() { return nullptr; }
|
||||
virtual Led* GetLed();
|
||||
virtual AudioCodec* GetAudioCodec() = 0;
|
||||
virtual bool GetTemperature(float& esp32temp);
|
||||
virtual Display* GetDisplay();
|
||||
virtual Camera* GetCamera();
|
||||
virtual Music* GetMusic();
|
||||
virtual NetworkInterface* GetNetwork() = 0;
|
||||
virtual void StartNetwork() = 0;
|
||||
virtual const char* GetNetworkStateIcon() = 0;
|
||||
virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging);
|
||||
virtual std::string GetJson();
|
||||
virtual void SetPowerSaveMode(bool enabled) = 0;
|
||||
virtual std::string GetBoardJson() = 0;
|
||||
virtual std::string GetDeviceStatusJson() = 0;
|
||||
};
|
||||
|
||||
#define DECLARE_BOARD(BOARD_CLASS_NAME) \
|
||||
void* create_board() { \
|
||||
return new BOARD_CLASS_NAME(); \
|
||||
}
|
||||
|
||||
#endif // BOARD_H
|
||||
125
main/boards/common/button.cc
Normal file
125
main/boards/common/button.cc
Normal file
@@ -0,0 +1,125 @@
|
||||
#include "button.h"
|
||||
|
||||
#include <button_gpio.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
#define TAG "Button"
|
||||
|
||||
#if CONFIG_SOC_ADC_SUPPORTED
|
||||
AdcButton::AdcButton(const button_adc_config_t& adc_config) : Button(nullptr) {
|
||||
button_config_t btn_config = {
|
||||
.long_press_time = 2000,
|
||||
.short_press_time = 0,
|
||||
};
|
||||
ESP_ERROR_CHECK(iot_button_new_adc_device(&btn_config, &adc_config, &button_handle_));
|
||||
}
|
||||
#endif
|
||||
|
||||
Button::Button(button_handle_t button_handle) : button_handle_(button_handle) {
|
||||
}
|
||||
|
||||
Button::Button(gpio_num_t gpio_num, bool active_high, uint16_t long_press_time, uint16_t short_press_time, bool enable_power_save) : gpio_num_(gpio_num) {
|
||||
if (gpio_num == GPIO_NUM_NC) {
|
||||
return;
|
||||
}
|
||||
button_config_t button_config = {
|
||||
.long_press_time = long_press_time,
|
||||
.short_press_time = short_press_time
|
||||
};
|
||||
button_gpio_config_t gpio_config = {
|
||||
.gpio_num = gpio_num,
|
||||
.active_level = static_cast<uint8_t>(active_high ? 1 : 0),
|
||||
.enable_power_save = enable_power_save,
|
||||
.disable_pull = false
|
||||
};
|
||||
ESP_ERROR_CHECK(iot_button_new_gpio_device(&button_config, &gpio_config, &button_handle_));
|
||||
}
|
||||
|
||||
Button::~Button() {
|
||||
if (button_handle_ != NULL) {
|
||||
iot_button_delete(button_handle_);
|
||||
}
|
||||
}
|
||||
|
||||
void Button::OnPressDown(std::function<void()> callback) {
|
||||
if (button_handle_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
on_press_down_ = callback;
|
||||
iot_button_register_cb(button_handle_, BUTTON_PRESS_DOWN, nullptr, [](void* handle, void* usr_data) {
|
||||
Button* button = static_cast<Button*>(usr_data);
|
||||
if (button->on_press_down_) {
|
||||
button->on_press_down_();
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
void Button::OnPressUp(std::function<void()> callback) {
|
||||
if (button_handle_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
on_press_up_ = callback;
|
||||
iot_button_register_cb(button_handle_, BUTTON_PRESS_UP, nullptr, [](void* handle, void* usr_data) {
|
||||
Button* button = static_cast<Button*>(usr_data);
|
||||
if (button->on_press_up_) {
|
||||
button->on_press_up_();
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
void Button::OnLongPress(std::function<void()> callback) {
|
||||
if (button_handle_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
on_long_press_ = callback;
|
||||
iot_button_register_cb(button_handle_, BUTTON_LONG_PRESS_START, nullptr, [](void* handle, void* usr_data) {
|
||||
Button* button = static_cast<Button*>(usr_data);
|
||||
if (button->on_long_press_) {
|
||||
button->on_long_press_();
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
void Button::OnClick(std::function<void()> callback) {
|
||||
if (button_handle_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
on_click_ = callback;
|
||||
iot_button_register_cb(button_handle_, BUTTON_SINGLE_CLICK, nullptr, [](void* handle, void* usr_data) {
|
||||
Button* button = static_cast<Button*>(usr_data);
|
||||
if (button->on_click_) {
|
||||
button->on_click_();
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
void Button::OnDoubleClick(std::function<void()> callback) {
|
||||
if (button_handle_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
on_double_click_ = callback;
|
||||
iot_button_register_cb(button_handle_, BUTTON_DOUBLE_CLICK, nullptr, [](void* handle, void* usr_data) {
|
||||
Button* button = static_cast<Button*>(usr_data);
|
||||
if (button->on_double_click_) {
|
||||
button->on_double_click_();
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
void Button::OnMultipleClick(std::function<void()> callback, uint8_t click_count) {
|
||||
if (button_handle_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
on_multiple_click_ = callback;
|
||||
button_event_args_t event_args = {
|
||||
.multiple_clicks = {
|
||||
.clicks = click_count
|
||||
}
|
||||
};
|
||||
iot_button_register_cb(button_handle_, BUTTON_MULTIPLE_CLICK, &event_args, [](void* handle, void* usr_data) {
|
||||
Button* button = static_cast<Button*>(usr_data);
|
||||
if (button->on_multiple_click_) {
|
||||
button->on_multiple_click_();
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
49
main/boards/common/button.h
Normal file
49
main/boards/common/button.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#ifndef BUTTON_H_
|
||||
#define BUTTON_H_
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <iot_button.h>
|
||||
#include <button_types.h>
|
||||
#include <button_adc.h>
|
||||
#include <button_gpio.h>
|
||||
#include <functional>
|
||||
|
||||
class Button {
|
||||
public:
|
||||
Button(button_handle_t button_handle);
|
||||
Button(gpio_num_t gpio_num, bool active_high = false, uint16_t long_press_time = 0, uint16_t short_press_time = 0, bool enable_power_save = false);
|
||||
~Button();
|
||||
|
||||
void OnPressDown(std::function<void()> callback);
|
||||
void OnPressUp(std::function<void()> callback);
|
||||
void OnLongPress(std::function<void()> callback);
|
||||
void OnClick(std::function<void()> callback);
|
||||
void OnDoubleClick(std::function<void()> callback);
|
||||
void OnMultipleClick(std::function<void()> callback, uint8_t click_count = 3);
|
||||
|
||||
protected:
|
||||
gpio_num_t gpio_num_;
|
||||
button_handle_t button_handle_ = nullptr;
|
||||
|
||||
std::function<void()> on_press_down_;
|
||||
std::function<void()> on_press_up_;
|
||||
std::function<void()> on_long_press_;
|
||||
std::function<void()> on_click_;
|
||||
std::function<void()> on_double_click_;
|
||||
std::function<void()> on_multiple_click_;
|
||||
};
|
||||
|
||||
#if CONFIG_SOC_ADC_SUPPORTED
|
||||
class AdcButton : public Button {
|
||||
public:
|
||||
AdcButton(const button_adc_config_t& adc_config);
|
||||
};
|
||||
#endif
|
||||
|
||||
class PowerSaveButton : public Button {
|
||||
public:
|
||||
PowerSaveButton(gpio_num_t gpio_num) : Button(gpio_num, false, 0, 0, true) {
|
||||
}
|
||||
};
|
||||
|
||||
#endif // BUTTON_H_
|
||||
15
main/boards/common/camera.h
Normal file
15
main/boards/common/camera.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#ifndef CAMERA_H
|
||||
#define CAMERA_H
|
||||
|
||||
#include <string>
|
||||
|
||||
class Camera {
|
||||
public:
|
||||
virtual void SetExplainUrl(const std::string& url, const std::string& token) = 0;
|
||||
virtual bool Capture() = 0;
|
||||
virtual bool SetHMirror(bool enabled) = 0;
|
||||
virtual bool SetVFlip(bool enabled) = 0;
|
||||
virtual std::string Explain(const std::string& question) = 0;
|
||||
};
|
||||
|
||||
#endif // CAMERA_H
|
||||
93
main/boards/common/dual_network_board.cc
Normal file
93
main/boards/common/dual_network_board.cc
Normal file
@@ -0,0 +1,93 @@
|
||||
#include "dual_network_board.h"
|
||||
#include "application.h"
|
||||
#include "display.h"
|
||||
#include "assets/lang_config.h"
|
||||
#include "settings.h"
|
||||
#include <esp_log.h>
|
||||
|
||||
static const char *TAG = "DualNetworkBoard";
|
||||
|
||||
DualNetworkBoard::DualNetworkBoard(gpio_num_t ml307_tx_pin, gpio_num_t ml307_rx_pin, gpio_num_t ml307_dtr_pin, int32_t default_net_type)
|
||||
: Board(),
|
||||
ml307_tx_pin_(ml307_tx_pin),
|
||||
ml307_rx_pin_(ml307_rx_pin),
|
||||
ml307_dtr_pin_(ml307_dtr_pin) {
|
||||
|
||||
// 从Settings加载网络类型
|
||||
network_type_ = LoadNetworkTypeFromSettings(default_net_type);
|
||||
|
||||
// 只初始化当前网络类型对应的板卡
|
||||
InitializeCurrentBoard();
|
||||
}
|
||||
|
||||
NetworkType DualNetworkBoard::LoadNetworkTypeFromSettings(int32_t default_net_type) {
|
||||
Settings settings("network", true);
|
||||
int network_type = settings.GetInt("type", default_net_type); // 默认使用ML307 (1)
|
||||
return network_type == 1 ? NetworkType::ML307 : NetworkType::WIFI;
|
||||
}
|
||||
|
||||
void DualNetworkBoard::SaveNetworkTypeToSettings(NetworkType type) {
|
||||
Settings settings("network", true);
|
||||
int network_type = (type == NetworkType::ML307) ? 1 : 0;
|
||||
settings.SetInt("type", network_type);
|
||||
}
|
||||
|
||||
void DualNetworkBoard::InitializeCurrentBoard() {
|
||||
if (network_type_ == NetworkType::ML307) {
|
||||
ESP_LOGI(TAG, "Initialize ML307 board");
|
||||
current_board_ = std::make_unique<Ml307Board>(ml307_tx_pin_, ml307_rx_pin_, ml307_dtr_pin_);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Initialize WiFi board");
|
||||
current_board_ = std::make_unique<WifiBoard>();
|
||||
}
|
||||
}
|
||||
|
||||
void DualNetworkBoard::SwitchNetworkType() {
|
||||
auto display = GetDisplay();
|
||||
if (network_type_ == NetworkType::WIFI) {
|
||||
SaveNetworkTypeToSettings(NetworkType::ML307);
|
||||
display->ShowNotification(Lang::Strings::SWITCH_TO_4G_NETWORK);
|
||||
} else {
|
||||
SaveNetworkTypeToSettings(NetworkType::WIFI);
|
||||
display->ShowNotification(Lang::Strings::SWITCH_TO_WIFI_NETWORK);
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
auto& app = Application::GetInstance();
|
||||
app.Reboot();
|
||||
}
|
||||
|
||||
|
||||
std::string DualNetworkBoard::GetBoardType() {
|
||||
return current_board_->GetBoardType();
|
||||
}
|
||||
|
||||
void DualNetworkBoard::StartNetwork() {
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
|
||||
if (network_type_ == NetworkType::WIFI) {
|
||||
display->SetStatus(Lang::Strings::CONNECTING);
|
||||
} else {
|
||||
display->SetStatus(Lang::Strings::DETECTING_MODULE);
|
||||
}
|
||||
current_board_->StartNetwork();
|
||||
}
|
||||
|
||||
NetworkInterface* DualNetworkBoard::GetNetwork() {
|
||||
return current_board_->GetNetwork();
|
||||
}
|
||||
|
||||
const char* DualNetworkBoard::GetNetworkStateIcon() {
|
||||
return current_board_->GetNetworkStateIcon();
|
||||
}
|
||||
|
||||
void DualNetworkBoard::SetPowerSaveMode(bool enabled) {
|
||||
current_board_->SetPowerSaveMode(enabled);
|
||||
}
|
||||
|
||||
std::string DualNetworkBoard::GetBoardJson() {
|
||||
return current_board_->GetBoardJson();
|
||||
}
|
||||
|
||||
std::string DualNetworkBoard::GetDeviceStatusJson() {
|
||||
return current_board_->GetDeviceStatusJson();
|
||||
}
|
||||
59
main/boards/common/dual_network_board.h
Normal file
59
main/boards/common/dual_network_board.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#ifndef DUAL_NETWORK_BOARD_H
|
||||
#define DUAL_NETWORK_BOARD_H
|
||||
|
||||
#include "board.h"
|
||||
#include "wifi_board.h"
|
||||
#include "ml307_board.h"
|
||||
#include <memory>
|
||||
|
||||
//enum NetworkType
|
||||
enum class NetworkType {
|
||||
WIFI,
|
||||
ML307
|
||||
};
|
||||
|
||||
// 双网络板卡类,可以在WiFi和ML307之间切换
|
||||
class DualNetworkBoard : public Board {
|
||||
private:
|
||||
// 使用基类指针存储当前活动的板卡
|
||||
std::unique_ptr<Board> current_board_;
|
||||
NetworkType network_type_ = NetworkType::ML307; // Default to ML307
|
||||
|
||||
// ML307的引脚配置
|
||||
gpio_num_t ml307_tx_pin_;
|
||||
gpio_num_t ml307_rx_pin_;
|
||||
gpio_num_t ml307_dtr_pin_;
|
||||
|
||||
// 从Settings加载网络类型
|
||||
NetworkType LoadNetworkTypeFromSettings(int32_t default_net_type);
|
||||
|
||||
// 保存网络类型到Settings
|
||||
void SaveNetworkTypeToSettings(NetworkType type);
|
||||
|
||||
// 初始化当前网络类型对应的板卡
|
||||
void InitializeCurrentBoard();
|
||||
|
||||
public:
|
||||
DualNetworkBoard(gpio_num_t ml307_tx_pin, gpio_num_t ml307_rx_pin, gpio_num_t ml307_dtr_pin = GPIO_NUM_NC, int32_t default_net_type = 1);
|
||||
virtual ~DualNetworkBoard() = default;
|
||||
|
||||
// 切换网络类型
|
||||
void SwitchNetworkType();
|
||||
|
||||
// 获取当前网络类型
|
||||
NetworkType GetNetworkType() const { return network_type_; }
|
||||
|
||||
// 获取当前活动的板卡引用
|
||||
Board& GetCurrentBoard() const { return *current_board_; }
|
||||
|
||||
// 重写Board接口
|
||||
virtual std::string GetBoardType() override;
|
||||
virtual void StartNetwork() override;
|
||||
virtual NetworkInterface* GetNetwork() override;
|
||||
virtual const char* GetNetworkStateIcon() override;
|
||||
virtual void SetPowerSaveMode(bool enabled) override;
|
||||
virtual std::string GetBoardJson() override;
|
||||
virtual std::string GetDeviceStatusJson() override;
|
||||
};
|
||||
|
||||
#endif // DUAL_NETWORK_BOARD_H
|
||||
302
main/boards/common/esp32_camera.cc
Normal file
302
main/boards/common/esp32_camera.cc
Normal file
@@ -0,0 +1,302 @@
|
||||
#include "esp32_camera.h"
|
||||
#include "mcp_server.h"
|
||||
#include "display.h"
|
||||
#include "board.h"
|
||||
#include "system_info.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <img_converters.h>
|
||||
#include <cstring>
|
||||
|
||||
#define TAG "Esp32Camera"
|
||||
|
||||
Esp32Camera::Esp32Camera(const camera_config_t& config) {
|
||||
// camera init
|
||||
esp_err_t err = esp_camera_init(&config); // 配置上面定义的参数
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Camera init failed with error 0x%x", err);
|
||||
return;
|
||||
}
|
||||
|
||||
sensor_t *s = esp_camera_sensor_get(); // 获取摄像头型号
|
||||
if (s->id.PID == GC0308_PID) {
|
||||
s->set_hmirror(s, 0); // 这里控制摄像头镜像 写1镜像 写0不镜像
|
||||
}
|
||||
|
||||
// 初始化预览图片的内存
|
||||
memset(&preview_image_, 0, sizeof(preview_image_));
|
||||
preview_image_.header.magic = LV_IMAGE_HEADER_MAGIC;
|
||||
preview_image_.header.cf = LV_COLOR_FORMAT_RGB565;
|
||||
preview_image_.header.flags = 0;
|
||||
|
||||
switch (config.frame_size) {
|
||||
case FRAMESIZE_SVGA:
|
||||
preview_image_.header.w = 800;
|
||||
preview_image_.header.h = 600;
|
||||
break;
|
||||
case FRAMESIZE_VGA:
|
||||
preview_image_.header.w = 640;
|
||||
preview_image_.header.h = 480;
|
||||
break;
|
||||
case FRAMESIZE_QVGA:
|
||||
preview_image_.header.w = 320;
|
||||
preview_image_.header.h = 240;
|
||||
break;
|
||||
case FRAMESIZE_128X128:
|
||||
preview_image_.header.w = 128;
|
||||
preview_image_.header.h = 128;
|
||||
break;
|
||||
case FRAMESIZE_240X240:
|
||||
preview_image_.header.w = 240;
|
||||
preview_image_.header.h = 240;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Unsupported frame size: %d, image preview will not be shown", config.frame_size);
|
||||
preview_image_.data_size = 0;
|
||||
preview_image_.data = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
preview_image_.header.stride = preview_image_.header.w * 2;
|
||||
preview_image_.data_size = preview_image_.header.w * preview_image_.header.h * 2;
|
||||
preview_image_.data = (uint8_t*)heap_caps_malloc(preview_image_.data_size, MALLOC_CAP_SPIRAM);
|
||||
if (preview_image_.data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for preview image");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Esp32Camera::~Esp32Camera() {
|
||||
if (fb_) {
|
||||
esp_camera_fb_return(fb_);
|
||||
fb_ = nullptr;
|
||||
}
|
||||
if (preview_image_.data) {
|
||||
heap_caps_free((void*)preview_image_.data);
|
||||
preview_image_.data = nullptr;
|
||||
}
|
||||
esp_camera_deinit();
|
||||
}
|
||||
|
||||
void Esp32Camera::SetExplainUrl(const std::string& url, const std::string& token) {
|
||||
explain_url_ = url;
|
||||
explain_token_ = token;
|
||||
}
|
||||
|
||||
bool Esp32Camera::Capture() {
|
||||
if (encoder_thread_.joinable()) {
|
||||
encoder_thread_.join();
|
||||
}
|
||||
|
||||
int frames_to_get = 2;
|
||||
// Try to get a stable frame
|
||||
for (int i = 0; i < frames_to_get; i++) {
|
||||
if (fb_ != nullptr) {
|
||||
esp_camera_fb_return(fb_);
|
||||
}
|
||||
fb_ = esp_camera_fb_get();
|
||||
if (fb_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Camera capture failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果预览图片 buffer 为空,则跳过预览
|
||||
// 但仍返回 true,因为此时图像可以上传至服务器
|
||||
if (preview_image_.data_size == 0) {
|
||||
ESP_LOGW(TAG, "Skip preview because of unsupported frame size");
|
||||
return true;
|
||||
}
|
||||
if (preview_image_.data == nullptr) {
|
||||
ESP_LOGE(TAG, "Preview image data is not initialized");
|
||||
return true;
|
||||
}
|
||||
// 显示预览图片
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
if (display != nullptr) {
|
||||
auto src = (uint16_t*)fb_->buf;
|
||||
auto dst = (uint16_t*)preview_image_.data;
|
||||
size_t pixel_count = fb_->len / 2;
|
||||
for (size_t i = 0; i < pixel_count; i++) {
|
||||
// 交换每个16位字内的字节
|
||||
dst[i] = __builtin_bswap16(src[i]);
|
||||
}
|
||||
display->SetPreviewImage(&preview_image_);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool Esp32Camera::SetHMirror(bool enabled) {
|
||||
sensor_t *s = esp_camera_sensor_get();
|
||||
if (s == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to get camera sensor");
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t err = s->set_hmirror(s, enabled);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set horizontal mirror: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Camera horizontal mirror set to: %s", enabled ? "enabled" : "disabled");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Esp32Camera::SetVFlip(bool enabled) {
|
||||
sensor_t *s = esp_camera_sensor_get();
|
||||
if (s == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to get camera sensor");
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t err = s->set_vflip(s, enabled);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set vertical flip: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Camera vertical flip set to: %s", enabled ? "enabled" : "disabled");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将摄像头捕获的图像发送到远程服务器进行AI分析和解释
|
||||
*
|
||||
* 该函数将当前摄像头缓冲区中的图像编码为JPEG格式,并通过HTTP POST请求
|
||||
* 以multipart/form-data的形式发送到指定的解释服务器。服务器将根据提供的
|
||||
* 问题对图像进行AI分析并返回结果。
|
||||
*
|
||||
* 实现特点:
|
||||
* - 使用独立线程编码JPEG,与主线程分离
|
||||
* - 采用分块传输编码(chunked transfer encoding)优化内存使用
|
||||
* - 通过队列机制实现编码线程和发送线程的数据同步
|
||||
* - 支持设备ID、客户端ID和认证令牌的HTTP头部配置
|
||||
*
|
||||
* @param question 要向AI提出的关于图像的问题,将作为表单字段发送
|
||||
* @return std::string 服务器返回的JSON格式响应字符串
|
||||
* 成功时包含AI分析结果,失败时包含错误信息
|
||||
* 格式示例:{"success": true, "result": "分析结果"}
|
||||
* {"success": false, "message": "错误信息"}
|
||||
*
|
||||
* @note 调用此函数前必须先调用SetExplainUrl()设置服务器URL
|
||||
* @note 函数会等待之前的编码线程完成后再开始新的处理
|
||||
* @warning 如果摄像头缓冲区为空或网络连接失败,将返回错误信息
|
||||
*/
|
||||
std::string Esp32Camera::Explain(const std::string& question) {
|
||||
if (explain_url_.empty()) {
|
||||
return "{\"success\": false, \"message\": \"Image explain URL or token is not set\"}";
|
||||
}
|
||||
|
||||
// 创建局部的 JPEG 队列, 40 entries is about to store 512 * 40 = 20480 bytes of JPEG data
|
||||
QueueHandle_t jpeg_queue = xQueueCreate(40, sizeof(JpegChunk));
|
||||
if (jpeg_queue == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create JPEG queue");
|
||||
return "{\"success\": false, \"message\": \"Failed to create JPEG queue\"}";
|
||||
}
|
||||
|
||||
// We spawn a thread to encode the image to JPEG
|
||||
encoder_thread_ = std::thread([this, jpeg_queue]() {
|
||||
frame2jpg_cb(fb_, 80, [](void* arg, size_t index, const void* data, size_t len) -> unsigned int {
|
||||
auto jpeg_queue = (QueueHandle_t)arg;
|
||||
JpegChunk chunk = {
|
||||
.data = (uint8_t*)heap_caps_aligned_alloc(16, len, MALLOC_CAP_SPIRAM),
|
||||
.len = len
|
||||
};
|
||||
memcpy(chunk.data, data, len);
|
||||
xQueueSend(jpeg_queue, &chunk, portMAX_DELAY);
|
||||
return len;
|
||||
}, jpeg_queue);
|
||||
});
|
||||
|
||||
auto network = Board::GetInstance().GetNetwork();
|
||||
auto http = network->CreateHttp(3);
|
||||
// 构造multipart/form-data请求体
|
||||
std::string boundary = "----ESP32_CAMERA_BOUNDARY";
|
||||
|
||||
// 配置HTTP客户端,使用分块传输编码
|
||||
http->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
|
||||
http->SetHeader("Client-Id", Board::GetInstance().GetUuid().c_str());
|
||||
if (!explain_token_.empty()) {
|
||||
http->SetHeader("Authorization", "Bearer " + explain_token_);
|
||||
}
|
||||
http->SetHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
|
||||
http->SetHeader("Transfer-Encoding", "chunked");
|
||||
if (!http->Open("POST", explain_url_)) {
|
||||
ESP_LOGE(TAG, "Failed to connect to explain URL");
|
||||
// Clear the queue
|
||||
encoder_thread_.join();
|
||||
JpegChunk chunk;
|
||||
while (xQueueReceive(jpeg_queue, &chunk, portMAX_DELAY) == pdPASS) {
|
||||
if (chunk.data != nullptr) {
|
||||
heap_caps_free(chunk.data);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
vQueueDelete(jpeg_queue);
|
||||
return "{\"success\": false, \"message\": \"Failed to connect to explain URL\"}";
|
||||
}
|
||||
|
||||
{
|
||||
// 第一块:question字段
|
||||
std::string question_field;
|
||||
question_field += "--" + boundary + "\r\n";
|
||||
question_field += "Content-Disposition: form-data; name=\"question\"\r\n";
|
||||
question_field += "\r\n";
|
||||
question_field += question + "\r\n";
|
||||
http->Write(question_field.c_str(), question_field.size());
|
||||
}
|
||||
{
|
||||
// 第二块:文件字段头部
|
||||
std::string file_header;
|
||||
file_header += "--" + boundary + "\r\n";
|
||||
file_header += "Content-Disposition: form-data; name=\"file\"; filename=\"camera.jpg\"\r\n";
|
||||
file_header += "Content-Type: image/jpeg\r\n";
|
||||
file_header += "\r\n";
|
||||
http->Write(file_header.c_str(), file_header.size());
|
||||
}
|
||||
|
||||
// 第三块:JPEG数据
|
||||
size_t total_sent = 0;
|
||||
while (true) {
|
||||
JpegChunk chunk;
|
||||
if (xQueueReceive(jpeg_queue, &chunk, portMAX_DELAY) != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to receive JPEG chunk");
|
||||
break;
|
||||
}
|
||||
if (chunk.data == nullptr) {
|
||||
break; // The last chunk
|
||||
}
|
||||
http->Write((const char*)chunk.data, chunk.len);
|
||||
total_sent += chunk.len;
|
||||
heap_caps_free(chunk.data);
|
||||
}
|
||||
// Wait for the encoder thread to finish
|
||||
encoder_thread_.join();
|
||||
// 清理队列
|
||||
vQueueDelete(jpeg_queue);
|
||||
|
||||
{
|
||||
// 第四块:multipart尾部
|
||||
std::string multipart_footer;
|
||||
multipart_footer += "\r\n--" + boundary + "--\r\n";
|
||||
http->Write(multipart_footer.c_str(), multipart_footer.size());
|
||||
}
|
||||
// 结束块
|
||||
http->Write("", 0);
|
||||
|
||||
if (http->GetStatusCode() != 200) {
|
||||
ESP_LOGE(TAG, "Failed to upload photo, status code: %d", http->GetStatusCode());
|
||||
return "{\"success\": false, \"message\": \"Failed to upload photo\"}";
|
||||
}
|
||||
|
||||
std::string result = http->ReadAll();
|
||||
http->Close();
|
||||
|
||||
// Get remain task stack size
|
||||
size_t remain_stack_size = uxTaskGetStackHighWaterMark(nullptr);
|
||||
ESP_LOGI(TAG, "Explain image size=%dx%d, compressed size=%d, remain stack size=%d, question=%s\n%s",
|
||||
fb_->width, fb_->height, total_sent, remain_stack_size, question.c_str(), result.c_str());
|
||||
return result;
|
||||
}
|
||||
39
main/boards/common/esp32_camera.h
Normal file
39
main/boards/common/esp32_camera.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#ifndef ESP32_CAMERA_H
|
||||
#define ESP32_CAMERA_H
|
||||
|
||||
#include <esp_camera.h>
|
||||
#include <lvgl.h>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/queue.h>
|
||||
|
||||
#include "camera.h"
|
||||
|
||||
struct JpegChunk {
|
||||
uint8_t* data;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
class Esp32Camera : public Camera {
|
||||
private:
|
||||
camera_fb_t* fb_ = nullptr;
|
||||
lv_img_dsc_t preview_image_;
|
||||
std::string explain_url_;
|
||||
std::string explain_token_;
|
||||
std::thread encoder_thread_;
|
||||
|
||||
public:
|
||||
Esp32Camera(const camera_config_t& config);
|
||||
~Esp32Camera();
|
||||
|
||||
virtual void SetExplainUrl(const std::string& url, const std::string& token);
|
||||
virtual bool Capture();
|
||||
// 翻转控制函数
|
||||
virtual bool SetHMirror(bool enabled) override;
|
||||
virtual bool SetVFlip(bool enabled) override;
|
||||
virtual std::string Explain(const std::string& question);
|
||||
};
|
||||
|
||||
#endif // ESP32_CAMERA_H
|
||||
1600
main/boards/common/esp32_music.cc
Normal file
1600
main/boards/common/esp32_music.cc
Normal file
File diff suppressed because it is too large
Load Diff
96
main/boards/common/esp32_music.h
Normal file
96
main/boards/common/esp32_music.h
Normal file
@@ -0,0 +1,96 @@
|
||||
#ifndef ESP32_MUSIC_H
|
||||
#define ESP32_MUSIC_H
|
||||
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <vector>
|
||||
|
||||
#include "music.h"
|
||||
|
||||
// MP3解码器支持
|
||||
extern "C" {
|
||||
#include "mp3dec.h"
|
||||
}
|
||||
|
||||
// 音频数据块结构
|
||||
struct AudioChunk {
|
||||
uint8_t* data;
|
||||
size_t size;
|
||||
|
||||
AudioChunk() : data(nullptr), size(0) {}
|
||||
AudioChunk(uint8_t* d, size_t s) : data(d), size(s) {}
|
||||
};
|
||||
|
||||
class Esp32Music : public Music {
|
||||
private:
|
||||
std::string last_downloaded_data_;
|
||||
std::string current_music_url_;
|
||||
std::string current_song_name_;
|
||||
bool song_name_displayed_;
|
||||
|
||||
// 歌词相关
|
||||
std::string current_lyric_url_;
|
||||
std::vector<std::pair<int, std::string>> lyrics_; // 时间戳和歌词文本
|
||||
std::mutex lyrics_mutex_; // 保护lyrics_数组的互斥锁
|
||||
std::atomic<int> current_lyric_index_;
|
||||
std::thread lyric_thread_;
|
||||
std::atomic<bool> is_lyric_running_;
|
||||
std::atomic<bool> is_playing_;
|
||||
std::atomic<bool> is_downloading_;
|
||||
std::thread play_thread_;
|
||||
std::thread download_thread_;
|
||||
int64_t current_play_time_ms_; // 当前播放时间(毫秒)
|
||||
int64_t last_frame_time_ms_; // 上一帧的时间戳
|
||||
int total_frames_decoded_; // 已解码的帧数
|
||||
|
||||
// 音频缓冲区
|
||||
std::queue<AudioChunk> audio_buffer_;
|
||||
std::mutex buffer_mutex_;
|
||||
std::condition_variable buffer_cv_;
|
||||
size_t buffer_size_;
|
||||
static constexpr size_t MAX_BUFFER_SIZE = 256 * 1024; // 256KB缓冲区(降低以减少brownout风险)
|
||||
static constexpr size_t MIN_BUFFER_SIZE = 32 * 1024; // 32KB最小播放缓冲(降低以减少brownout风险)
|
||||
|
||||
// MP3解码器相关
|
||||
HMP3Decoder mp3_decoder_;
|
||||
MP3FrameInfo mp3_frame_info_;
|
||||
bool mp3_decoder_initialized_;
|
||||
|
||||
// 私有方法
|
||||
void DownloadAudioStream(const std::string& music_url);
|
||||
void PlayAudioStream();
|
||||
void ClearAudioBuffer();
|
||||
bool InitializeMp3Decoder();
|
||||
void CleanupMp3Decoder();
|
||||
void ResetSampleRate(); // 重置采样率到原始值
|
||||
|
||||
// 歌词相关私有方法
|
||||
bool DownloadLyrics(const std::string& lyric_url);
|
||||
bool ParseLyrics(const std::string& lyric_content);
|
||||
void LyricDisplayThread();
|
||||
void UpdateLyricDisplay(int64_t current_time_ms);
|
||||
|
||||
// ID3标签处理
|
||||
size_t SkipId3Tag(uint8_t* data, size_t size);
|
||||
|
||||
public:
|
||||
Esp32Music();
|
||||
~Esp32Music();
|
||||
|
||||
virtual bool Download(const std::string& song_name) override;
|
||||
virtual bool Play() override;
|
||||
virtual bool Stop() override;
|
||||
virtual std::string GetDownloadResult() override;
|
||||
|
||||
// 新增方法
|
||||
virtual bool StartStreaming(const std::string& music_url) override;
|
||||
virtual bool StopStreaming() override; // 停止流式播放
|
||||
virtual size_t GetBufferSize() const override { return buffer_size_; }
|
||||
virtual bool IsDownloading() const override { return is_downloading_; }
|
||||
};
|
||||
|
||||
#endif // ESP32_MUSIC_H
|
||||
35
main/boards/common/i2c_device.cc
Normal file
35
main/boards/common/i2c_device.cc
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "i2c_device.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
|
||||
#define TAG "I2cDevice"
|
||||
|
||||
|
||||
I2cDevice::I2cDevice(i2c_master_bus_handle_t i2c_bus, uint8_t addr) {
|
||||
i2c_device_config_t i2c_device_cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = addr,
|
||||
.scl_speed_hz = 400 * 1000,
|
||||
.scl_wait_us = 0,
|
||||
.flags = {
|
||||
.disable_ack_check = 0,
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_bus, &i2c_device_cfg, &i2c_device_));
|
||||
assert(i2c_device_ != NULL);
|
||||
}
|
||||
|
||||
void I2cDevice::WriteReg(uint8_t reg, uint8_t value) {
|
||||
uint8_t buffer[2] = {reg, value};
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(i2c_device_, buffer, 2, 100));
|
||||
}
|
||||
|
||||
uint8_t I2cDevice::ReadReg(uint8_t reg) {
|
||||
uint8_t buffer[1];
|
||||
ESP_ERROR_CHECK(i2c_master_transmit_receive(i2c_device_, ®, 1, buffer, 1, 100));
|
||||
return buffer[0];
|
||||
}
|
||||
|
||||
void I2cDevice::ReadRegs(uint8_t reg, uint8_t* buffer, size_t length) {
|
||||
ESP_ERROR_CHECK(i2c_master_transmit_receive(i2c_device_, ®, 1, buffer, length, 100));
|
||||
}
|
||||
18
main/boards/common/i2c_device.h
Normal file
18
main/boards/common/i2c_device.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef I2C_DEVICE_H
|
||||
#define I2C_DEVICE_H
|
||||
|
||||
#include <driver/i2c_master.h>
|
||||
|
||||
class I2cDevice {
|
||||
public:
|
||||
I2cDevice(i2c_master_bus_handle_t i2c_bus, uint8_t addr);
|
||||
|
||||
protected:
|
||||
i2c_master_dev_handle_t i2c_device_;
|
||||
|
||||
void WriteReg(uint8_t reg, uint8_t value);
|
||||
uint8_t ReadReg(uint8_t reg);
|
||||
void ReadRegs(uint8_t reg, uint8_t* buffer, size_t length);
|
||||
};
|
||||
|
||||
#endif // I2C_DEVICE_H
|
||||
52
main/boards/common/knob.cc
Normal file
52
main/boards/common/knob.cc
Normal file
@@ -0,0 +1,52 @@
|
||||
#include "knob.h"
|
||||
|
||||
static const char* TAG = "Knob";
|
||||
|
||||
Knob::Knob(gpio_num_t pin_a, gpio_num_t pin_b) {
|
||||
knob_config_t config = {
|
||||
.default_direction = 0,
|
||||
.gpio_encoder_a = static_cast<uint8_t>(pin_a),
|
||||
.gpio_encoder_b = static_cast<uint8_t>(pin_b),
|
||||
};
|
||||
|
||||
esp_err_t err = ESP_OK;
|
||||
knob_handle_ = iot_knob_create(&config);
|
||||
if (knob_handle_ == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create knob instance");
|
||||
return;
|
||||
}
|
||||
|
||||
err = iot_knob_register_cb(knob_handle_, KNOB_LEFT, knob_callback, this);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to register left callback: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
err = iot_knob_register_cb(knob_handle_, KNOB_RIGHT, knob_callback, this);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to register right callback: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Knob initialized with pins A:%d B:%d", pin_a, pin_b);
|
||||
}
|
||||
|
||||
Knob::~Knob() {
|
||||
if (knob_handle_ != NULL) {
|
||||
iot_knob_delete(knob_handle_);
|
||||
knob_handle_ = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void Knob::OnRotate(std::function<void(bool)> callback) {
|
||||
on_rotate_ = callback;
|
||||
}
|
||||
|
||||
void Knob::knob_callback(void* arg, void* data) {
|
||||
Knob* knob = static_cast<Knob*>(data);
|
||||
knob_event_t event = iot_knob_get_event(arg);
|
||||
|
||||
if (knob->on_rotate_) {
|
||||
knob->on_rotate_(event == KNOB_RIGHT);
|
||||
}
|
||||
}
|
||||
25
main/boards/common/knob.h
Normal file
25
main/boards/common/knob.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#ifndef KNOB_H_
|
||||
#define KNOB_H_
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <functional>
|
||||
#include <esp_log.h>
|
||||
#include <iot_knob.h>
|
||||
|
||||
class Knob {
|
||||
public:
|
||||
Knob(gpio_num_t pin_a, gpio_num_t pin_b);
|
||||
~Knob();
|
||||
|
||||
void OnRotate(std::function<void(bool)> callback);
|
||||
|
||||
private:
|
||||
static void knob_callback(void* arg, void* data);
|
||||
|
||||
knob_handle_t knob_handle_;
|
||||
gpio_num_t pin_a_;
|
||||
gpio_num_t pin_b_;
|
||||
std::function<void(bool)> on_rotate_;
|
||||
};
|
||||
|
||||
#endif // KNOB_H_
|
||||
44
main/boards/common/lamp_controller.h
Normal file
44
main/boards/common/lamp_controller.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef __LAMP_CONTROLLER_H__
|
||||
#define __LAMP_CONTROLLER_H__
|
||||
|
||||
#include "mcp_server.h"
|
||||
|
||||
|
||||
class LampController {
|
||||
private:
|
||||
bool power_ = false;
|
||||
gpio_num_t gpio_num_;
|
||||
|
||||
public:
|
||||
LampController(gpio_num_t gpio_num) : gpio_num_(gpio_num) {
|
||||
gpio_config_t config = {
|
||||
.pin_bit_mask = (1ULL << gpio_num_),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
ESP_ERROR_CHECK(gpio_config(&config));
|
||||
gpio_set_level(gpio_num_, 0);
|
||||
|
||||
auto& mcp_server = McpServer::GetInstance();
|
||||
mcp_server.AddTool("self.lamp.get_state", "Get the power state of the lamp", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
|
||||
return power_ ? "{\"power\": true}" : "{\"power\": false}";
|
||||
});
|
||||
|
||||
mcp_server.AddTool("self.lamp.turn_on", "Turn on the lamp", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
|
||||
power_ = true;
|
||||
gpio_set_level(gpio_num_, 1);
|
||||
return true;
|
||||
});
|
||||
|
||||
mcp_server.AddTool("self.lamp.turn_off", "Turn off the lamp", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
|
||||
power_ = false;
|
||||
gpio_set_level(gpio_num_, 0);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
#endif // __LAMP_CONTROLLER_H__
|
||||
194
main/boards/common/ml307_board.cc
Normal file
194
main/boards/common/ml307_board.cc
Normal file
@@ -0,0 +1,194 @@
|
||||
#include "ml307_board.h"
|
||||
|
||||
#include "application.h"
|
||||
#include "display.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_timer.h>
|
||||
#include <font_awesome.h>
|
||||
#include <opus_encoder.h>
|
||||
|
||||
static const char *TAG = "Ml307Board";
|
||||
|
||||
Ml307Board::Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, gpio_num_t dtr_pin) : tx_pin_(tx_pin), rx_pin_(rx_pin), dtr_pin_(dtr_pin) {
|
||||
}
|
||||
|
||||
std::string Ml307Board::GetBoardType() {
|
||||
return "ml307";
|
||||
}
|
||||
|
||||
void Ml307Board::StartNetwork() {
|
||||
auto& application = Application::GetInstance();
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
display->SetStatus(Lang::Strings::DETECTING_MODULE);
|
||||
|
||||
while (true) {
|
||||
modem_ = AtModem::Detect(tx_pin_, rx_pin_, dtr_pin_, 921600);
|
||||
if (modem_ != nullptr) {
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
|
||||
modem_->OnNetworkStateChanged([this, &application](bool network_ready) {
|
||||
if (network_ready) {
|
||||
ESP_LOGI(TAG, "Network is ready");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Network is down");
|
||||
auto device_state = application.GetDeviceState();
|
||||
if (device_state == kDeviceStateListening || device_state == kDeviceStateSpeaking) {
|
||||
application.Schedule([this, &application]() {
|
||||
application.SetDeviceState(kDeviceStateIdle);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for network ready
|
||||
display->SetStatus(Lang::Strings::REGISTERING_NETWORK);
|
||||
while (true) {
|
||||
auto result = modem_->WaitForNetworkReady();
|
||||
if (result == NetworkStatus::ErrorInsertPin) {
|
||||
application.Alert(Lang::Strings::ERROR, Lang::Strings::PIN_ERROR, "triangle_exclamation", Lang::Sounds::OGG_ERR_PIN);
|
||||
} else if (result == NetworkStatus::ErrorRegistrationDenied) {
|
||||
application.Alert(Lang::Strings::ERROR, Lang::Strings::REG_ERROR, "triangle_exclamation", Lang::Sounds::OGG_ERR_REG);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
}
|
||||
|
||||
// Print the ML307 modem information
|
||||
std::string module_revision = modem_->GetModuleRevision();
|
||||
std::string imei = modem_->GetImei();
|
||||
std::string iccid = modem_->GetIccid();
|
||||
ESP_LOGI(TAG, "ML307 Revision: %s", module_revision.c_str());
|
||||
ESP_LOGI(TAG, "ML307 IMEI: %s", imei.c_str());
|
||||
ESP_LOGI(TAG, "ML307 ICCID: %s", iccid.c_str());
|
||||
}
|
||||
|
||||
NetworkInterface* Ml307Board::GetNetwork() {
|
||||
return modem_.get();
|
||||
}
|
||||
|
||||
const char* Ml307Board::GetNetworkStateIcon() {
|
||||
if (modem_ == nullptr || !modem_->network_ready()) {
|
||||
return FONT_AWESOME_SIGNAL_OFF;
|
||||
}
|
||||
int csq = modem_->GetCsq();
|
||||
if (csq == -1) {
|
||||
return FONT_AWESOME_SIGNAL_OFF;
|
||||
} else if (csq >= 0 && csq <= 14) {
|
||||
return FONT_AWESOME_SIGNAL_WEAK;
|
||||
} else if (csq >= 15 && csq <= 19) {
|
||||
return FONT_AWESOME_SIGNAL_FAIR;
|
||||
} else if (csq >= 20 && csq <= 24) {
|
||||
return FONT_AWESOME_SIGNAL_GOOD;
|
||||
} else if (csq >= 25 && csq <= 31) {
|
||||
return FONT_AWESOME_SIGNAL_STRONG;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Invalid CSQ: %d", csq);
|
||||
return FONT_AWESOME_SIGNAL_OFF;
|
||||
}
|
||||
|
||||
std::string Ml307Board::GetBoardJson() {
|
||||
// Set the board type for OTA
|
||||
std::string board_json = std::string("{\"type\":\"" BOARD_TYPE "\",");
|
||||
board_json += "\"name\":\"" BOARD_NAME "\",";
|
||||
board_json += "\"revision\":\"" + modem_->GetModuleRevision() + "\",";
|
||||
board_json += "\"carrier\":\"" + modem_->GetCarrierName() + "\",";
|
||||
board_json += "\"csq\":\"" + std::to_string(modem_->GetCsq()) + "\",";
|
||||
board_json += "\"imei\":\"" + modem_->GetImei() + "\",";
|
||||
board_json += "\"iccid\":\"" + modem_->GetIccid() + "\",";
|
||||
board_json += "\"cereg\":" + modem_->GetRegistrationState().ToString() + "}";
|
||||
return board_json;
|
||||
}
|
||||
|
||||
void Ml307Board::SetPowerSaveMode(bool enabled) {
|
||||
// TODO: Implement power save mode for ML307
|
||||
}
|
||||
|
||||
std::string Ml307Board::GetDeviceStatusJson() {
|
||||
/*
|
||||
* 返回设备状态JSON
|
||||
*
|
||||
* 返回的JSON结构如下:
|
||||
* {
|
||||
* "audio_speaker": {
|
||||
* "volume": 70
|
||||
* },
|
||||
* "screen": {
|
||||
* "brightness": 100,
|
||||
* "theme": "light"
|
||||
* },
|
||||
* "battery": {
|
||||
* "level": 50,
|
||||
* "charging": true
|
||||
* },
|
||||
* "network": {
|
||||
* "type": "cellular",
|
||||
* "carrier": "CHINA MOBILE",
|
||||
* "csq": 10
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
auto& board = Board::GetInstance();
|
||||
auto root = cJSON_CreateObject();
|
||||
|
||||
// Audio speaker
|
||||
auto audio_speaker = cJSON_CreateObject();
|
||||
auto audio_codec = board.GetAudioCodec();
|
||||
if (audio_codec) {
|
||||
cJSON_AddNumberToObject(audio_speaker, "volume", audio_codec->output_volume());
|
||||
}
|
||||
cJSON_AddItemToObject(root, "audio_speaker", audio_speaker);
|
||||
|
||||
// Screen brightness
|
||||
auto backlight = board.GetBacklight();
|
||||
auto screen = cJSON_CreateObject();
|
||||
if (backlight) {
|
||||
cJSON_AddNumberToObject(screen, "brightness", backlight->brightness());
|
||||
}
|
||||
auto display = board.GetDisplay();
|
||||
if (display && display->height() > 64) { // For LCD display only
|
||||
cJSON_AddStringToObject(screen, "theme", display->GetTheme().c_str());
|
||||
}
|
||||
cJSON_AddItemToObject(root, "screen", screen);
|
||||
|
||||
// Battery
|
||||
int battery_level = 0;
|
||||
bool charging = false;
|
||||
bool discharging = false;
|
||||
if (board.GetBatteryLevel(battery_level, charging, discharging)) {
|
||||
cJSON* battery = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(battery, "level", battery_level);
|
||||
cJSON_AddBoolToObject(battery, "charging", charging);
|
||||
cJSON_AddItemToObject(root, "battery", battery);
|
||||
}
|
||||
|
||||
// Network
|
||||
auto network = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(network, "type", "cellular");
|
||||
cJSON_AddStringToObject(network, "carrier", modem_->GetCarrierName().c_str());
|
||||
int csq = modem_->GetCsq();
|
||||
if (csq == -1) {
|
||||
cJSON_AddStringToObject(network, "signal", "unknown");
|
||||
} else if (csq >= 0 && csq <= 14) {
|
||||
cJSON_AddStringToObject(network, "signal", "very weak");
|
||||
} else if (csq >= 15 && csq <= 19) {
|
||||
cJSON_AddStringToObject(network, "signal", "weak");
|
||||
} else if (csq >= 20 && csq <= 24) {
|
||||
cJSON_AddStringToObject(network, "signal", "medium");
|
||||
} else if (csq >= 25 && csq <= 31) {
|
||||
cJSON_AddStringToObject(network, "signal", "strong");
|
||||
}
|
||||
cJSON_AddItemToObject(root, "network", network);
|
||||
|
||||
auto json_str = cJSON_PrintUnformatted(root);
|
||||
std::string json(json_str);
|
||||
cJSON_free(json_str);
|
||||
cJSON_Delete(root);
|
||||
return json;
|
||||
}
|
||||
29
main/boards/common/ml307_board.h
Normal file
29
main/boards/common/ml307_board.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#ifndef ML307_BOARD_H
|
||||
#define ML307_BOARD_H
|
||||
|
||||
#include <memory>
|
||||
#include <at_modem.h>
|
||||
#include "board.h"
|
||||
|
||||
|
||||
class Ml307Board : public Board {
|
||||
protected:
|
||||
std::unique_ptr<AtModem> modem_;
|
||||
gpio_num_t tx_pin_;
|
||||
gpio_num_t rx_pin_;
|
||||
gpio_num_t dtr_pin_;
|
||||
|
||||
virtual std::string GetBoardJson() override;
|
||||
|
||||
public:
|
||||
Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, gpio_num_t dtr_pin = GPIO_NUM_NC);
|
||||
virtual std::string GetBoardType() override;
|
||||
virtual void StartNetwork() override;
|
||||
virtual NetworkInterface* GetNetwork() override;
|
||||
virtual const char* GetNetworkStateIcon() override;
|
||||
virtual void SetPowerSaveMode(bool enabled) override;
|
||||
virtual AudioCodec* GetAudioCodec() override { return nullptr; }
|
||||
virtual std::string GetDeviceStatusJson() override;
|
||||
};
|
||||
|
||||
#endif // ML307_BOARD_H
|
||||
22
main/boards/common/music.h
Normal file
22
main/boards/common/music.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#ifndef MUSIC_H
|
||||
#define MUSIC_H
|
||||
|
||||
#include <string>
|
||||
|
||||
class Music {
|
||||
public:
|
||||
virtual ~Music() = default; // 添加虚析构函数
|
||||
|
||||
virtual bool Download(const std::string& song_name) = 0;
|
||||
virtual bool Play() = 0;
|
||||
virtual bool Stop() = 0;
|
||||
virtual std::string GetDownloadResult() = 0;
|
||||
|
||||
// 新增流式播放相关方法
|
||||
virtual bool StartStreaming(const std::string& music_url) = 0;
|
||||
virtual bool StopStreaming() = 0; // 停止流式播放
|
||||
virtual size_t GetBufferSize() const = 0;
|
||||
virtual bool IsDownloading() const = 0;
|
||||
};
|
||||
|
||||
#endif // MUSIC_H
|
||||
132
main/boards/common/power_save_timer.cc
Normal file
132
main/boards/common/power_save_timer.cc
Normal file
@@ -0,0 +1,132 @@
|
||||
#include "power_save_timer.h"
|
||||
#include "application.h"
|
||||
#include "settings.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
|
||||
#define TAG "PowerSaveTimer"
|
||||
|
||||
|
||||
PowerSaveTimer::PowerSaveTimer(int cpu_max_freq, int seconds_to_sleep, int seconds_to_shutdown)
|
||||
: cpu_max_freq_(cpu_max_freq), seconds_to_sleep_(seconds_to_sleep), seconds_to_shutdown_(seconds_to_shutdown) {
|
||||
esp_timer_create_args_t timer_args = {
|
||||
.callback = [](void* arg) {
|
||||
auto self = static_cast<PowerSaveTimer*>(arg);
|
||||
self->PowerSaveCheck();
|
||||
},
|
||||
.arg = this,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "power_save_timer",
|
||||
.skip_unhandled_events = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &power_save_timer_));
|
||||
}
|
||||
|
||||
PowerSaveTimer::~PowerSaveTimer() {
|
||||
esp_timer_stop(power_save_timer_);
|
||||
esp_timer_delete(power_save_timer_);
|
||||
}
|
||||
|
||||
void PowerSaveTimer::SetEnabled(bool enabled) {
|
||||
if (enabled && !enabled_) {
|
||||
Settings settings("wifi", false);
|
||||
if (!settings.GetBool("sleep_mode", true)) {
|
||||
ESP_LOGI(TAG, "Power save timer is disabled by settings");
|
||||
return;
|
||||
}
|
||||
|
||||
ticks_ = 0;
|
||||
enabled_ = enabled;
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(power_save_timer_, 1000000));
|
||||
ESP_LOGI(TAG, "Power save timer enabled");
|
||||
} else if (!enabled && enabled_) {
|
||||
ESP_ERROR_CHECK(esp_timer_stop(power_save_timer_));
|
||||
enabled_ = enabled;
|
||||
WakeUp();
|
||||
ESP_LOGI(TAG, "Power save timer disabled");
|
||||
}
|
||||
}
|
||||
|
||||
void PowerSaveTimer::OnEnterSleepMode(std::function<void()> callback) {
|
||||
on_enter_sleep_mode_ = callback;
|
||||
}
|
||||
|
||||
void PowerSaveTimer::OnExitSleepMode(std::function<void()> callback) {
|
||||
on_exit_sleep_mode_ = callback;
|
||||
}
|
||||
|
||||
void PowerSaveTimer::OnShutdownRequest(std::function<void()> callback) {
|
||||
on_shutdown_request_ = callback;
|
||||
}
|
||||
|
||||
void PowerSaveTimer::PowerSaveCheck() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (!in_sleep_mode_ && !app.CanEnterSleepMode()) {
|
||||
ticks_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
ticks_++;
|
||||
if (seconds_to_sleep_ != -1 && ticks_ >= seconds_to_sleep_) {
|
||||
if (!in_sleep_mode_) {
|
||||
ESP_LOGI(TAG, "Enabling power save mode");
|
||||
in_sleep_mode_ = true;
|
||||
if (on_enter_sleep_mode_) {
|
||||
on_enter_sleep_mode_();
|
||||
}
|
||||
|
||||
if (cpu_max_freq_ != -1) {
|
||||
// Disable wake word detection
|
||||
auto& audio_service = app.GetAudioService();
|
||||
is_wake_word_running_ = audio_service.IsWakeWordRunning();
|
||||
if (is_wake_word_running_) {
|
||||
audio_service.EnableWakeWordDetection(false);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
// Disable audio input
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
if (codec) {
|
||||
codec->EnableInput(false);
|
||||
}
|
||||
|
||||
esp_pm_config_t pm_config = {
|
||||
.max_freq_mhz = cpu_max_freq_,
|
||||
.min_freq_mhz = 40,
|
||||
.light_sleep_enable = true,
|
||||
};
|
||||
esp_pm_configure(&pm_config);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (seconds_to_shutdown_ != -1 && ticks_ >= seconds_to_shutdown_ && on_shutdown_request_) {
|
||||
on_shutdown_request_();
|
||||
}
|
||||
}
|
||||
|
||||
void PowerSaveTimer::WakeUp() {
|
||||
ticks_ = 0;
|
||||
if (in_sleep_mode_) {
|
||||
ESP_LOGI(TAG, "Exiting power save mode");
|
||||
in_sleep_mode_ = false;
|
||||
|
||||
if (cpu_max_freq_ != -1) {
|
||||
esp_pm_config_t pm_config = {
|
||||
.max_freq_mhz = cpu_max_freq_,
|
||||
.min_freq_mhz = cpu_max_freq_,
|
||||
.light_sleep_enable = false,
|
||||
};
|
||||
esp_pm_configure(&pm_config);
|
||||
|
||||
// Enable wake word detection
|
||||
auto& app = Application::GetInstance();
|
||||
auto& audio_service = app.GetAudioService();
|
||||
if (is_wake_word_running_) {
|
||||
audio_service.EnableWakeWordDetection(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (on_exit_sleep_mode_) {
|
||||
on_exit_sleep_mode_();
|
||||
}
|
||||
}
|
||||
}
|
||||
34
main/boards/common/power_save_timer.h
Normal file
34
main/boards/common/power_save_timer.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <esp_timer.h>
|
||||
#include <esp_pm.h>
|
||||
|
||||
class PowerSaveTimer {
|
||||
public:
|
||||
PowerSaveTimer(int cpu_max_freq, int seconds_to_sleep = 20, int seconds_to_shutdown = -1);
|
||||
~PowerSaveTimer();
|
||||
|
||||
void SetEnabled(bool enabled);
|
||||
void OnEnterSleepMode(std::function<void()> callback);
|
||||
void OnExitSleepMode(std::function<void()> callback);
|
||||
void OnShutdownRequest(std::function<void()> callback);
|
||||
void WakeUp();
|
||||
|
||||
private:
|
||||
void PowerSaveCheck();
|
||||
|
||||
esp_timer_handle_t power_save_timer_ = nullptr;
|
||||
bool enabled_ = false;
|
||||
bool in_sleep_mode_ = false;
|
||||
bool is_wake_word_running_ = false;
|
||||
int ticks_ = 0;
|
||||
int cpu_max_freq_;
|
||||
int seconds_to_sleep_;
|
||||
int seconds_to_shutdown_;
|
||||
|
||||
std::function<void()> on_enter_sleep_mode_;
|
||||
std::function<void()> on_exit_sleep_mode_;
|
||||
std::function<void()> on_shutdown_request_;
|
||||
};
|
||||
57
main/boards/common/press_to_talk_mcp_tool.cc
Normal file
57
main/boards/common/press_to_talk_mcp_tool.cc
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "press_to_talk_mcp_tool.h"
|
||||
#include <esp_log.h>
|
||||
|
||||
static const char* TAG = "PressToTalkMcpTool";
|
||||
|
||||
PressToTalkMcpTool::PressToTalkMcpTool()
|
||||
: press_to_talk_enabled_(false) {
|
||||
}
|
||||
|
||||
void PressToTalkMcpTool::Initialize() {
|
||||
// 从设置中读取当前状态
|
||||
Settings settings("vendor");
|
||||
press_to_talk_enabled_ = settings.GetInt("press_to_talk", 0) != 0;
|
||||
|
||||
// 注册MCP工具
|
||||
auto& mcp_server = McpServer::GetInstance();
|
||||
mcp_server.AddTool("self.set_press_to_talk",
|
||||
"Switch between press to talk mode (长按说话) and click to talk mode (单击说话).\n"
|
||||
"The mode can be `press_to_talk` or `click_to_talk`.",
|
||||
PropertyList({
|
||||
Property("mode", kPropertyTypeString)
|
||||
}),
|
||||
[this](const PropertyList& properties) -> ReturnValue {
|
||||
return HandleSetPressToTalk(properties);
|
||||
});
|
||||
|
||||
ESP_LOGI(TAG, "PressToTalkMcpTool initialized, current mode: %s",
|
||||
press_to_talk_enabled_ ? "press_to_talk" : "click_to_talk");
|
||||
}
|
||||
|
||||
bool PressToTalkMcpTool::IsPressToTalkEnabled() const {
|
||||
return press_to_talk_enabled_;
|
||||
}
|
||||
|
||||
ReturnValue PressToTalkMcpTool::HandleSetPressToTalk(const PropertyList& properties) {
|
||||
auto mode = properties["mode"].value<std::string>();
|
||||
|
||||
if (mode == "press_to_talk") {
|
||||
SetPressToTalkEnabled(true);
|
||||
ESP_LOGI(TAG, "Switched to press to talk mode");
|
||||
return true;
|
||||
} else if (mode == "click_to_talk") {
|
||||
SetPressToTalkEnabled(false);
|
||||
ESP_LOGI(TAG, "Switched to click to talk mode");
|
||||
return true;
|
||||
}
|
||||
|
||||
throw std::runtime_error("Invalid mode: " + mode);
|
||||
}
|
||||
|
||||
void PressToTalkMcpTool::SetPressToTalkEnabled(bool enabled) {
|
||||
press_to_talk_enabled_ = enabled;
|
||||
|
||||
Settings settings("vendor", true);
|
||||
settings.SetInt("press_to_talk", enabled ? 1 : 0);
|
||||
ESP_LOGI(TAG, "Press to talk enabled: %d", enabled);
|
||||
}
|
||||
29
main/boards/common/press_to_talk_mcp_tool.h
Normal file
29
main/boards/common/press_to_talk_mcp_tool.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#ifndef PRESS_TO_TALK_MCP_TOOL_H
|
||||
#define PRESS_TO_TALK_MCP_TOOL_H
|
||||
|
||||
#include "mcp_server.h"
|
||||
#include "settings.h"
|
||||
|
||||
// 可复用的按键说话模式MCP工具类
|
||||
class PressToTalkMcpTool {
|
||||
private:
|
||||
bool press_to_talk_enabled_;
|
||||
|
||||
public:
|
||||
PressToTalkMcpTool();
|
||||
|
||||
// 初始化工具,注册到MCP服务器
|
||||
void Initialize();
|
||||
|
||||
// 获取当前按键说话模式状态
|
||||
bool IsPressToTalkEnabled() const;
|
||||
|
||||
private:
|
||||
// MCP工具的回调函数
|
||||
ReturnValue HandleSetPressToTalk(const PropertyList& properties);
|
||||
|
||||
// 内部方法:设置press to talk状态并保存到设置
|
||||
void SetPressToTalkEnabled(bool enabled);
|
||||
};
|
||||
|
||||
#endif // PRESS_TO_TALK_MCP_TOOL_H
|
||||
133
main/boards/common/sleep_timer.cc
Normal file
133
main/boards/common/sleep_timer.cc
Normal file
@@ -0,0 +1,133 @@
|
||||
#include "sleep_timer.h"
|
||||
#include "application.h"
|
||||
#include "board.h"
|
||||
#include "display.h"
|
||||
#include "settings.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_sleep.h>
|
||||
#include <esp_lvgl_port.h>
|
||||
|
||||
#define TAG "SleepTimer"
|
||||
|
||||
|
||||
SleepTimer::SleepTimer(int seconds_to_light_sleep, int seconds_to_deep_sleep)
|
||||
: seconds_to_light_sleep_(seconds_to_light_sleep), seconds_to_deep_sleep_(seconds_to_deep_sleep) {
|
||||
esp_timer_create_args_t timer_args = {
|
||||
.callback = [](void* arg) {
|
||||
auto self = static_cast<SleepTimer*>(arg);
|
||||
self->CheckTimer();
|
||||
},
|
||||
.arg = this,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "sleep_timer",
|
||||
.skip_unhandled_events = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &sleep_timer_));
|
||||
}
|
||||
|
||||
SleepTimer::~SleepTimer() {
|
||||
esp_timer_stop(sleep_timer_);
|
||||
esp_timer_delete(sleep_timer_);
|
||||
}
|
||||
|
||||
void SleepTimer::SetEnabled(bool enabled) {
|
||||
if (enabled && !enabled_) {
|
||||
Settings settings("wifi", false);
|
||||
if (!settings.GetBool("sleep_mode", true)) {
|
||||
ESP_LOGI(TAG, "Power save timer is disabled by settings");
|
||||
return;
|
||||
}
|
||||
|
||||
ticks_ = 0;
|
||||
enabled_ = enabled;
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(sleep_timer_, 1000000));
|
||||
ESP_LOGI(TAG, "Sleep timer enabled");
|
||||
} else if (!enabled && enabled_) {
|
||||
ESP_ERROR_CHECK(esp_timer_stop(sleep_timer_));
|
||||
enabled_ = enabled;
|
||||
WakeUp();
|
||||
ESP_LOGI(TAG, "Sleep timer disabled");
|
||||
}
|
||||
}
|
||||
|
||||
void SleepTimer::OnEnterLightSleepMode(std::function<void()> callback) {
|
||||
on_enter_light_sleep_mode_ = callback;
|
||||
}
|
||||
|
||||
void SleepTimer::OnExitLightSleepMode(std::function<void()> callback) {
|
||||
on_exit_light_sleep_mode_ = callback;
|
||||
}
|
||||
|
||||
void SleepTimer::OnEnterDeepSleepMode(std::function<void()> callback) {
|
||||
on_enter_deep_sleep_mode_ = callback;
|
||||
}
|
||||
|
||||
void SleepTimer::CheckTimer() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (!app.CanEnterSleepMode()) {
|
||||
ticks_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
ticks_++;
|
||||
if (seconds_to_light_sleep_ != -1 && ticks_ >= seconds_to_light_sleep_) {
|
||||
if (!in_light_sleep_mode_) {
|
||||
in_light_sleep_mode_ = true;
|
||||
if (on_enter_light_sleep_mode_) {
|
||||
on_enter_light_sleep_mode_();
|
||||
}
|
||||
|
||||
auto& audio_service = app.GetAudioService();
|
||||
bool is_wake_word_running = audio_service.IsWakeWordRunning();
|
||||
if (is_wake_word_running) {
|
||||
audio_service.EnableWakeWordDetection(false);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
app.Schedule([this, &app]() {
|
||||
while (in_light_sleep_mode_) {
|
||||
auto& board = Board::GetInstance();
|
||||
board.GetDisplay()->UpdateStatusBar(true);
|
||||
lv_refr_now(nullptr);
|
||||
lvgl_port_stop();
|
||||
|
||||
// 配置timer唤醒源(30秒后自动唤醒)
|
||||
esp_sleep_enable_timer_wakeup(30 * 1000000);
|
||||
|
||||
// 进入light sleep模式
|
||||
esp_light_sleep_start();
|
||||
lvgl_port_resume();
|
||||
|
||||
auto wakeup_reason = esp_sleep_get_wakeup_cause();
|
||||
ESP_LOGI(TAG, "Wake up from light sleep, wakeup_reason: %d", wakeup_reason);
|
||||
if (wakeup_reason != ESP_SLEEP_WAKEUP_TIMER) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
WakeUp();
|
||||
});
|
||||
|
||||
if (is_wake_word_running) {
|
||||
audio_service.EnableWakeWordDetection(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (seconds_to_deep_sleep_ != -1 && ticks_ >= seconds_to_deep_sleep_) {
|
||||
if (on_enter_deep_sleep_mode_) {
|
||||
on_enter_deep_sleep_mode_();
|
||||
}
|
||||
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
}
|
||||
|
||||
void SleepTimer::WakeUp() {
|
||||
ticks_ = 0;
|
||||
if (in_light_sleep_mode_) {
|
||||
in_light_sleep_mode_ = false;
|
||||
if (on_exit_light_sleep_mode_) {
|
||||
on_exit_light_sleep_mode_();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
main/boards/common/sleep_timer.h
Normal file
32
main/boards/common/sleep_timer.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <esp_timer.h>
|
||||
#include <esp_pm.h>
|
||||
|
||||
class SleepTimer {
|
||||
public:
|
||||
SleepTimer(int seconds_to_light_sleep = 20, int seconds_to_deep_sleep = -1);
|
||||
~SleepTimer();
|
||||
|
||||
void SetEnabled(bool enabled);
|
||||
void OnEnterLightSleepMode(std::function<void()> callback);
|
||||
void OnExitLightSleepMode(std::function<void()> callback);
|
||||
void OnEnterDeepSleepMode(std::function<void()> callback);
|
||||
void WakeUp();
|
||||
|
||||
private:
|
||||
void CheckTimer();
|
||||
|
||||
esp_timer_handle_t sleep_timer_ = nullptr;
|
||||
bool enabled_ = false;
|
||||
int ticks_ = 0;
|
||||
int seconds_to_light_sleep_;
|
||||
int seconds_to_deep_sleep_;
|
||||
bool in_light_sleep_mode_ = false;
|
||||
|
||||
std::function<void()> on_enter_light_sleep_mode_;
|
||||
std::function<void()> on_exit_light_sleep_mode_;
|
||||
std::function<void()> on_enter_deep_sleep_mode_;
|
||||
};
|
||||
65
main/boards/common/sy6970.cc
Normal file
65
main/boards/common/sy6970.cc
Normal file
@@ -0,0 +1,65 @@
|
||||
#include "sy6970.h"
|
||||
#include "board.h"
|
||||
#include "display.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
|
||||
#define TAG "Sy6970"
|
||||
|
||||
Sy6970::Sy6970(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
|
||||
}
|
||||
|
||||
int Sy6970::GetChangingStatus() {
|
||||
return (ReadReg(0x0B) >> 3) & 0x03;
|
||||
}
|
||||
|
||||
bool Sy6970::IsCharging() {
|
||||
return GetChangingStatus() != 0;
|
||||
}
|
||||
|
||||
bool Sy6970::IsPowerGood() {
|
||||
return (ReadReg(0x0B) & 0x04) != 0;
|
||||
}
|
||||
|
||||
bool Sy6970::IsChargingDone() {
|
||||
return GetChangingStatus() == 3;
|
||||
}
|
||||
|
||||
int Sy6970::GetBatteryVoltage() {
|
||||
uint8_t value = ReadReg(0x0E);
|
||||
value &= 0x7F;
|
||||
if (value == 0) {
|
||||
return 0;
|
||||
}
|
||||
return value * 20 + 2304;
|
||||
}
|
||||
|
||||
int Sy6970::GetChargeTargetVoltage() {
|
||||
uint8_t value = ReadReg(0x06);
|
||||
value = (value & 0xFC) >> 2;
|
||||
if (value > 0x30) {
|
||||
return 4608;
|
||||
}
|
||||
return value * 16 + 3840;
|
||||
}
|
||||
|
||||
int Sy6970::GetBatteryLevel() {
|
||||
int level = 0;
|
||||
// 电池所能掉电的最低电压
|
||||
int battery_minimum_voltage = 3200;
|
||||
int battery_voltage = GetBatteryVoltage();
|
||||
int charge_voltage_limit = GetChargeTargetVoltage();
|
||||
// ESP_LOGI(TAG, "battery_voltage: %d, charge_voltage_limit: %d", battery_voltage, charge_voltage_limit);
|
||||
if (battery_voltage > battery_minimum_voltage && charge_voltage_limit > battery_minimum_voltage) {
|
||||
level = (((float) battery_voltage - (float) battery_minimum_voltage) / ((float) charge_voltage_limit - (float) battery_minimum_voltage)) * 100.0;
|
||||
}
|
||||
// 不连接电池时读取的充电状态不稳定且battery_voltage有时会超过charge_voltage_limit
|
||||
if (level > 100) {
|
||||
level = 100;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
void Sy6970::PowerOff() {
|
||||
WriteReg(0x09, 0B01100100);
|
||||
}
|
||||
21
main/boards/common/sy6970.h
Normal file
21
main/boards/common/sy6970.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef __SY6970_H__
|
||||
#define __SY6970_H__
|
||||
|
||||
#include "i2c_device.h"
|
||||
|
||||
class Sy6970 : public I2cDevice {
|
||||
public:
|
||||
Sy6970(i2c_master_bus_handle_t i2c_bus, uint8_t addr);
|
||||
bool IsCharging();
|
||||
bool IsPowerGood();
|
||||
bool IsChargingDone();
|
||||
int GetBatteryLevel();
|
||||
void PowerOff();
|
||||
|
||||
private:
|
||||
int GetChangingStatus();
|
||||
int GetBatteryVoltage();
|
||||
int GetChargeTargetVoltage();
|
||||
};
|
||||
|
||||
#endif
|
||||
72
main/boards/common/system_reset.cc
Normal file
72
main/boards/common/system_reset.cc
Normal file
@@ -0,0 +1,72 @@
|
||||
#include "system_reset.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_partition.h>
|
||||
#include <esp_system.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
|
||||
|
||||
#define TAG "SystemReset"
|
||||
|
||||
|
||||
SystemReset::SystemReset(gpio_num_t reset_nvs_pin, gpio_num_t reset_factory_pin) : reset_nvs_pin_(reset_nvs_pin), reset_factory_pin_(reset_factory_pin) {
|
||||
// Configure GPIO1, GPIO2 as INPUT, reset NVS flash if the button is pressed
|
||||
gpio_config_t io_conf;
|
||||
io_conf.intr_type = GPIO_INTR_DISABLE;
|
||||
io_conf.mode = GPIO_MODE_INPUT;
|
||||
io_conf.pin_bit_mask = (1ULL << reset_nvs_pin_) | (1ULL << reset_factory_pin_);
|
||||
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||||
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
|
||||
gpio_config(&io_conf);
|
||||
}
|
||||
|
||||
|
||||
void SystemReset::CheckButtons() {
|
||||
if (gpio_get_level(reset_factory_pin_) == 0) {
|
||||
ESP_LOGI(TAG, "Button is pressed, reset to factory");
|
||||
ResetNvsFlash();
|
||||
ResetToFactory();
|
||||
}
|
||||
|
||||
if (gpio_get_level(reset_nvs_pin_) == 0) {
|
||||
ESP_LOGI(TAG, "Button is pressed, reset NVS flash");
|
||||
ResetNvsFlash();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemReset::ResetNvsFlash() {
|
||||
ESP_LOGI(TAG, "Resetting NVS flash");
|
||||
esp_err_t ret = nvs_flash_erase();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to erase NVS flash");
|
||||
}
|
||||
ret = nvs_flash_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to initialize NVS flash");
|
||||
}
|
||||
}
|
||||
|
||||
void SystemReset::ResetToFactory() {
|
||||
ESP_LOGI(TAG, "Resetting to factory");
|
||||
// Erase otadata partition
|
||||
const esp_partition_t* partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_OTA, NULL);
|
||||
if (partition == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to find otadata partition");
|
||||
return;
|
||||
}
|
||||
esp_partition_erase_range(partition, 0, partition->size);
|
||||
ESP_LOGI(TAG, "Erased otadata partition");
|
||||
|
||||
// Reboot in 3 seconds
|
||||
RestartInSeconds(3);
|
||||
}
|
||||
|
||||
void SystemReset::RestartInSeconds(int seconds) {
|
||||
for (int i = seconds; i > 0; i--) {
|
||||
ESP_LOGI(TAG, "Resetting in %d seconds", i);
|
||||
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||
}
|
||||
esp_restart();
|
||||
}
|
||||
21
main/boards/common/system_reset.h
Normal file
21
main/boards/common/system_reset.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef _SYSTEM_RESET_H
|
||||
#define _SYSTEM_RESET_H
|
||||
|
||||
#include <driver/gpio.h>
|
||||
|
||||
class SystemReset {
|
||||
public:
|
||||
SystemReset(gpio_num_t reset_nvs_pin, gpio_num_t reset_factory_pin); // 构造函数私有化
|
||||
void CheckButtons();
|
||||
|
||||
private:
|
||||
gpio_num_t reset_nvs_pin_;
|
||||
gpio_num_t reset_factory_pin_;
|
||||
|
||||
void ResetNvsFlash();
|
||||
void ResetToFactory();
|
||||
void RestartInSeconds(int seconds);
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
262
main/boards/common/wifi_board.cc
Normal file
262
main/boards/common/wifi_board.cc
Normal file
@@ -0,0 +1,262 @@
|
||||
#include "wifi_board.h"
|
||||
|
||||
#include "display.h"
|
||||
#include "application.h"
|
||||
#include "system_info.h"
|
||||
#include "settings.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <esp_network.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
#include <font_awesome.h>
|
||||
#include <wifi_station.h>
|
||||
#include <wifi_configuration_ap.h>
|
||||
#include <ssid_manager.h>
|
||||
#include "afsk_demod.h"
|
||||
|
||||
static const char *TAG = "WifiBoard";
|
||||
|
||||
WifiBoard::WifiBoard() {
|
||||
Settings settings("wifi", true);
|
||||
wifi_config_mode_ = settings.GetInt("force_ap") == 1;
|
||||
if (wifi_config_mode_) {
|
||||
ESP_LOGI(TAG, "force_ap is set to 1, reset to 0");
|
||||
settings.SetInt("force_ap", 0);
|
||||
}
|
||||
}
|
||||
|
||||
std::string WifiBoard::GetBoardType() {
|
||||
return "wifi";
|
||||
}
|
||||
|
||||
void WifiBoard::EnterWifiConfigMode() {
|
||||
auto& application = Application::GetInstance();
|
||||
application.SetDeviceState(kDeviceStateWifiConfiguring);
|
||||
|
||||
auto& wifi_ap = WifiConfigurationAp::GetInstance();
|
||||
wifi_ap.SetLanguage(Lang::CODE);
|
||||
wifi_ap.SetSsidPrefix("Xiaozhi");
|
||||
wifi_ap.Start();
|
||||
|
||||
// 显示 WiFi 配置 AP 的 SSID 和 Web 服务器 URL
|
||||
std::string hint = Lang::Strings::CONNECT_TO_HOTSPOT;
|
||||
hint += wifi_ap.GetSsid();
|
||||
hint += Lang::Strings::ACCESS_VIA_BROWSER;
|
||||
hint += wifi_ap.GetWebServerUrl();
|
||||
hint += "\n\n";
|
||||
|
||||
// 播报配置 WiFi 的提示
|
||||
application.Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "gear", Lang::Sounds::OGG_WIFICONFIG);
|
||||
|
||||
#if CONFIG_USE_ACOUSTIC_WIFI_PROVISIONING
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
int channel = 1;
|
||||
if (codec) {
|
||||
channel = codec->input_channels();
|
||||
}
|
||||
ESP_LOGI(TAG, "Start receiving WiFi credentials from audio, input channels: %d", channel);
|
||||
audio_wifi_config::ReceiveWifiCredentialsFromAudio(&application, &wifi_ap, display, channel);
|
||||
#endif
|
||||
|
||||
// Wait forever until reset after configuration
|
||||
while (true) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
}
|
||||
}
|
||||
|
||||
void WifiBoard::StartNetwork() {
|
||||
// User can press BOOT button while starting to enter WiFi configuration mode
|
||||
if (wifi_config_mode_) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
|
||||
// If no WiFi SSID is configured, enter WiFi configuration mode
|
||||
auto& ssid_manager = SsidManager::GetInstance();
|
||||
auto ssid_list = ssid_manager.GetSsidList();
|
||||
if (ssid_list.empty()) {
|
||||
wifi_config_mode_ = true;
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
|
||||
auto& wifi_station = WifiStation::GetInstance();
|
||||
wifi_station.OnScanBegin([this]() {
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
display->ShowNotification(Lang::Strings::SCANNING_WIFI, 30000);
|
||||
});
|
||||
wifi_station.OnConnect([this](const std::string& ssid) {
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
std::string notification = Lang::Strings::CONNECT_TO;
|
||||
notification += ssid;
|
||||
notification += "...";
|
||||
display->ShowNotification(notification.c_str(), 30000);
|
||||
});
|
||||
wifi_station.OnConnected([this](const std::string& ssid) {
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
std::string notification = Lang::Strings::CONNECTED_TO;
|
||||
notification += ssid;
|
||||
display->ShowNotification(notification.c_str(), 30000);
|
||||
});
|
||||
wifi_station.Start();
|
||||
|
||||
// Try to connect to WiFi, if failed, launch the WiFi configuration AP
|
||||
if (!wifi_station.WaitForConnected(60 * 1000)) {
|
||||
wifi_station.Stop();
|
||||
wifi_config_mode_ = true;
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NetworkInterface* WifiBoard::GetNetwork() {
|
||||
static EspNetwork network;
|
||||
return &network;
|
||||
}
|
||||
|
||||
const char* WifiBoard::GetNetworkStateIcon() {
|
||||
if (wifi_config_mode_) {
|
||||
return FONT_AWESOME_WIFI;
|
||||
}
|
||||
auto& wifi_station = WifiStation::GetInstance();
|
||||
if (!wifi_station.IsConnected()) {
|
||||
return FONT_AWESOME_WIFI_SLASH;
|
||||
}
|
||||
int8_t rssi = wifi_station.GetRssi();
|
||||
if (rssi >= -60) {
|
||||
return FONT_AWESOME_WIFI;
|
||||
} else if (rssi >= -70) {
|
||||
return FONT_AWESOME_WIFI_FAIR;
|
||||
} else {
|
||||
return FONT_AWESOME_WIFI_WEAK;
|
||||
}
|
||||
}
|
||||
|
||||
std::string WifiBoard::GetBoardJson() {
|
||||
// Set the board type for OTA
|
||||
auto& wifi_station = WifiStation::GetInstance();
|
||||
std::string board_json = R"({)";
|
||||
board_json += R"("type":")" + std::string(BOARD_TYPE) + R"(",)";
|
||||
board_json += R"("name":")" + std::string(BOARD_NAME) + R"(",)";
|
||||
if (!wifi_config_mode_) {
|
||||
board_json += R"("ssid":")" + wifi_station.GetSsid() + R"(",)";
|
||||
board_json += R"("rssi":)" + std::to_string(wifi_station.GetRssi()) + R"(,)";
|
||||
board_json += R"("channel":)" + std::to_string(wifi_station.GetChannel()) + R"(,)";
|
||||
board_json += R"("ip":")" + wifi_station.GetIpAddress() + R"(",)";
|
||||
}
|
||||
board_json += R"("mac":")" + SystemInfo::GetMacAddress() + R"(")";
|
||||
board_json += R"(})";
|
||||
return board_json;
|
||||
}
|
||||
|
||||
void WifiBoard::SetPowerSaveMode(bool enabled) {
|
||||
auto& wifi_station = WifiStation::GetInstance();
|
||||
wifi_station.SetPowerSaveMode(enabled);
|
||||
}
|
||||
|
||||
void WifiBoard::ResetWifiConfiguration() {
|
||||
// Set a flag and reboot the device to enter the network configuration mode
|
||||
{
|
||||
Settings settings("wifi", true);
|
||||
settings.SetInt("force_ap", 1);
|
||||
}
|
||||
GetDisplay()->ShowNotification(Lang::Strings::ENTERING_WIFI_CONFIG_MODE);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
// Reboot the device
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
std::string WifiBoard::GetDeviceStatusJson() {
|
||||
/*
|
||||
* 返回设备状态JSON
|
||||
*
|
||||
* 返回的JSON结构如下:
|
||||
* {
|
||||
* "audio_speaker": {
|
||||
* "volume": 70
|
||||
* },
|
||||
* "screen": {
|
||||
* "brightness": 100,
|
||||
* "theme": "light"
|
||||
* },
|
||||
* "battery": {
|
||||
* "level": 50,
|
||||
* "charging": true
|
||||
* },
|
||||
* "network": {
|
||||
* "type": "wifi",
|
||||
* "ssid": "Xiaozhi",
|
||||
* "rssi": -60
|
||||
* },
|
||||
* "chip": {
|
||||
* "temperature": 25
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
auto& board = Board::GetInstance();
|
||||
auto root = cJSON_CreateObject();
|
||||
|
||||
// Audio speaker
|
||||
auto audio_speaker = cJSON_CreateObject();
|
||||
auto audio_codec = board.GetAudioCodec();
|
||||
if (audio_codec) {
|
||||
cJSON_AddNumberToObject(audio_speaker, "volume", audio_codec->output_volume());
|
||||
}
|
||||
cJSON_AddItemToObject(root, "audio_speaker", audio_speaker);
|
||||
|
||||
// Screen brightness
|
||||
auto backlight = board.GetBacklight();
|
||||
auto screen = cJSON_CreateObject();
|
||||
if (backlight) {
|
||||
cJSON_AddNumberToObject(screen, "brightness", backlight->brightness());
|
||||
}
|
||||
auto display = board.GetDisplay();
|
||||
if (display && display->height() > 64) { // For LCD display only
|
||||
cJSON_AddStringToObject(screen, "theme", display->GetTheme().c_str());
|
||||
}
|
||||
cJSON_AddItemToObject(root, "screen", screen);
|
||||
|
||||
// Battery
|
||||
int battery_level = 0;
|
||||
bool charging = false;
|
||||
bool discharging = false;
|
||||
if (board.GetBatteryLevel(battery_level, charging, discharging)) {
|
||||
cJSON* battery = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(battery, "level", battery_level);
|
||||
cJSON_AddBoolToObject(battery, "charging", charging);
|
||||
cJSON_AddItemToObject(root, "battery", battery);
|
||||
}
|
||||
|
||||
// Network
|
||||
auto network = cJSON_CreateObject();
|
||||
auto& wifi_station = WifiStation::GetInstance();
|
||||
cJSON_AddStringToObject(network, "type", "wifi");
|
||||
cJSON_AddStringToObject(network, "ssid", wifi_station.GetSsid().c_str());
|
||||
int rssi = wifi_station.GetRssi();
|
||||
if (rssi >= -60) {
|
||||
cJSON_AddStringToObject(network, "signal", "strong");
|
||||
} else if (rssi >= -70) {
|
||||
cJSON_AddStringToObject(network, "signal", "medium");
|
||||
} else {
|
||||
cJSON_AddStringToObject(network, "signal", "weak");
|
||||
}
|
||||
cJSON_AddItemToObject(root, "network", network);
|
||||
|
||||
// Chip
|
||||
float esp32temp = 0.0f;
|
||||
if (board.GetTemperature(esp32temp)) {
|
||||
auto chip = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(chip, "temperature", esp32temp);
|
||||
cJSON_AddItemToObject(root, "chip", chip);
|
||||
}
|
||||
|
||||
auto json_str = cJSON_PrintUnformatted(root);
|
||||
std::string json(json_str);
|
||||
cJSON_free(json_str);
|
||||
cJSON_Delete(root);
|
||||
return json;
|
||||
}
|
||||
24
main/boards/common/wifi_board.h
Normal file
24
main/boards/common/wifi_board.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#ifndef WIFI_BOARD_H
|
||||
#define WIFI_BOARD_H
|
||||
|
||||
#include "board.h"
|
||||
|
||||
class WifiBoard : public Board {
|
||||
protected:
|
||||
bool wifi_config_mode_ = false;
|
||||
void EnterWifiConfigMode();
|
||||
virtual std::string GetBoardJson() override;
|
||||
|
||||
public:
|
||||
WifiBoard();
|
||||
virtual std::string GetBoardType() override;
|
||||
virtual void StartNetwork() override;
|
||||
virtual NetworkInterface* GetNetwork() override;
|
||||
virtual const char* GetNetworkStateIcon() override;
|
||||
virtual void SetPowerSaveMode(bool enabled) override;
|
||||
virtual void ResetWifiConfiguration();
|
||||
virtual AudioCodec* GetAudioCodec() override { return nullptr; }
|
||||
virtual std::string GetDeviceStatusJson() override;
|
||||
};
|
||||
|
||||
#endif // WIFI_BOARD_H
|
||||
Reference in New Issue
Block a user