Initial commit

This commit is contained in:
2026-04-26 21:35:04 +08:00
commit da6ca1b09a
1483 changed files with 115719 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
#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 (only if it's not NC)
if (charging_pin_ != GPIO_NUM_NC) {
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_DEFAULT,
.adc_atten = ADC_ATTEN_DB_12,
},
.adc_channel = adc_channel,
.upper_resistor = upper_resistor,
.lower_resistor = lower_resistor
};
// 在ADC配置部分进行条件设置
if (charging_pin_ != GPIO_NUM_NC) {
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;
} else {
// 不设置回调让adc_battery_estimation库使用软件估算
adc_cfg.charging_detect_cb = nullptr;
adc_cfg.charging_detect_user_data = nullptr;
}
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_));
}
if (timer_handle_) {
esp_timer_stop(timer_handle_);
esp_timer_delete(timer_handle_);
}
}
bool AdcBatteryMonitor::IsCharging() {
// 优先使用adc_battery_estimation库的功能
if (adc_battery_estimation_handle_ != nullptr) {
bool is_charging = false;
esp_err_t err = adc_battery_estimation_get_charging_state(adc_battery_estimation_handle_, &is_charging);
if (err == ESP_OK) {
return is_charging;
}
}
// 回退到GPIO读取或返回默认值
if (charging_pin_ != GPIO_NUM_NC) {
return gpio_get_level(charging_pin_) == 1;
}
return false;
}
bool AdcBatteryMonitor::IsDischarging() {
return !IsCharging();
}
uint8_t AdcBatteryMonitor::GetBatteryLevel() {
// 如果句柄无效,返回默认值
if (adc_battery_estimation_handle_ == nullptr) {
return 100;
}
float capacity = 0;
esp_err_t err = adc_battery_estimation_get_capacity(adc_battery_estimation_handle_, &capacity);
if (err != ESP_OK) {
return 100; // 出错时返回默认值
}
return (uint8_t)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_);
}
}
}

View 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

View File

@@ -0,0 +1,375 @@
#include "afsk_demod.h"
#include <cstring>
#include <algorithm>
#include "esp_log.h"
#include "display.h"
#include "ssid_manager.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,
WifiManager *wifi_manager,
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;
}
// Save WiFi credentials using SsidManager
auto& ssid_manager = SsidManager::GetInstance();
ssid_manager.AddSsid(wifi_ssid, wifi_password);
ESP_LOGI(kLogTag, "WiFi credentials saved successfully");
// Exit config mode (triggers ConfigModeExit event)
wifi_manager->StopConfigAp();
data_buffer.decoded_text.reset(); // Clear processed data
return; // Exit the function
}
}
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;
}
}

View File

@@ -0,0 +1,177 @@
#pragma once
#include <vector>
#include <deque>
#include <string>
#include <memory>
#include <optional>
#include <cmath>
#include "wifi_manager.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, WifiManager *wifi_manager, 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;
}

View 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);
}

View 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

View 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);
}

View 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;
};

View File

@@ -0,0 +1,899 @@
#include "blufi.h"
#include <algorithm>
#include <cassert>
#include <cstring>
#include <string>
#include <vector>
#include "esp_bt.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/task.h"
#include "wifi_manager.h"
#define BLUFI_DEVICE_NAME "Xiaozhi-Blufi"
#ifdef CONFIG_BT_BLUEDROID_ENABLED
#include "esp_bt_device.h"
#include "esp_bt_main.h"
#include "esp_gap_ble_api.h"
#endif
#ifdef CONFIG_BT_NIMBLE_ENABLED
#include "console/console.h"
#include "host/ble_hs.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "services/gap/ble_svc_gap.h"
extern void esp_blufi_gatt_svr_register_cb(struct ble_gatt_register_ctxt* ctxt, void* arg);
extern int esp_blufi_gatt_svr_init(void);
extern void esp_blufi_gatt_svr_deinit(void);
extern void esp_blufi_btc_init(void);
extern void esp_blufi_btc_deinit(void);
#endif
extern "C" {
void esp_blufi_adv_start(void);
void esp_blufi_adv_stop(void);
void esp_blufi_disconnect(void);
void btc_blufi_report_error(esp_blufi_error_state_t state);
#ifdef CONFIG_BT_BLUEDROID_ENABLED
void esp_blufi_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t* param);
#endif
#ifdef CONFIG_BT_NIMBLE_ENABLED
void esp_blufi_gatt_svr_register_cb(struct ble_gatt_register_ctxt* ctxt, void* arg);
int esp_blufi_gatt_svr_init(void);
void esp_blufi_gatt_svr_deinit(void);
void esp_blufi_btc_init(void);
void esp_blufi_btc_deinit(void);
#endif
}
#include <wifi_station.h>
#include "esp_crc.h"
#include "esp_random.h"
#include "mbedtls/md5.h"
#include "ssid_manager.h"
static const char* BLUFI_TAG = "BLUFI_CLASS";
static wifi_mode_t GetWifiModeWithFallback(const WifiManager& wifi) {
if (wifi.IsConfigMode()) {
return WIFI_MODE_AP;
}
if (wifi.IsInitialized() && wifi.IsConnected()) {
return WIFI_MODE_STA;
}
wifi_mode_t mode = WIFI_MODE_STA;
esp_wifi_get_mode(&mode);
return mode;
}
Blufi& Blufi::GetInstance() {
static Blufi instance;
return instance;
}
Blufi::Blufi()
: m_sec(nullptr),
m_ble_is_connected(false),
m_sta_connected(false),
m_sta_got_ip(false),
m_provisioned(false),
m_deinited(false),
m_sta_ssid_len(0),
m_sta_is_connecting(false) {
memset(&m_sta_config, 0, sizeof(m_sta_config));
memset(m_sta_bssid, 0, sizeof(m_sta_bssid));
memset(m_sta_ssid, 0, sizeof(m_sta_ssid));
memset(&m_sta_conn_info, 0, sizeof(m_sta_conn_info));
}
Blufi::~Blufi() {
if (m_sec) {
_security_deinit();
}
}
esp_err_t Blufi::init() {
esp_err_t ret = ESP_FAIL;
inited_ = true;
m_provisioned = false;
m_deinited = false;
// Start WiFi scan early to have results ready when user connects
auto& wifi_manager = WifiManager::GetInstance();
if (!wifi_manager.IsInitialized() || !wifi_manager.IsConfigMode()) {
// start scan immediately
start_wifi_scan();
} else {
ESP_LOGE(BLUFI_TAG,
"Blufi and WiFi hotspot network configuration cannot "
"be used simultaneously.");
return ret;
}
#if CONFIG_BT_CONTROLLER_ENABLED || !CONFIG_BT_NIMBLE_ENABLED
ret = _controller_init();
if (ret) {
ESP_LOGE(BLUFI_TAG, "BLUFI controller init failed: %s", esp_err_to_name(ret));
return ret;
}
#endif
ret = _host_and_cb_init();
if (ret) {
ESP_LOGE(BLUFI_TAG, "BLUFI host and cb init failed: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(BLUFI_TAG, "BLUFI VERSION %04x", esp_blufi_get_version());
return ESP_OK;
}
esp_err_t Blufi::deinit() {
esp_err_t ret = ESP_OK;
if (inited_) {
if (m_deinited) {
return ESP_OK;
}
m_deinited = true;
ret = _host_deinit();
if (ret) {
ESP_LOGE(BLUFI_TAG, "Host deinit failed: %s", esp_err_to_name(ret));
}
#if CONFIG_BT_CONTROLLER_ENABLED || !CONFIG_BT_NIMBLE_ENABLED
ret = _controller_deinit();
if (ret) {
ESP_LOGE(BLUFI_TAG, "Controller deinit failed: %s", esp_err_to_name(ret));
}
#endif
}
return ret;
}
#ifdef CONFIG_BT_BLUEDROID_ENABLED
esp_err_t Blufi::_host_init() {
esp_err_t ret = esp_bluedroid_init();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s init bluedroid failed: %s", __func__, esp_err_to_name(ret));
return ESP_FAIL;
}
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s enable bluedroid failed: %s", __func__, esp_err_to_name(ret));
return ESP_FAIL;
}
ESP_LOGI(BLUFI_TAG, "BD ADDR: " ESP_BD_ADDR_STR, ESP_BD_ADDR_HEX(esp_bt_dev_get_address()));
return ESP_OK;
}
esp_err_t Blufi::_host_deinit() {
esp_err_t ret = esp_blufi_profile_deinit();
if (ret != ESP_OK)
return ret;
ret = esp_bluedroid_disable();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s disable bluedroid failed: %s", __func__, esp_err_to_name(ret));
return ESP_FAIL;
}
ret = esp_bluedroid_deinit();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s deinit bluedroid failed: %s", __func__, esp_err_to_name(ret));
return ESP_FAIL;
}
return ESP_OK;
}
esp_err_t Blufi::_gap_register_callback() {
esp_err_t rc = esp_ble_gap_register_callback(esp_blufi_gap_event_handler);
if (rc) {
return rc;
}
return esp_blufi_profile_init();
}
esp_err_t Blufi::_host_and_cb_init() {
static esp_blufi_callbacks_t blufi_callbacks = {
.event_cb = &_event_callback_trampoline,
.negotiate_data_handler = &_negotiate_data_handler_trampoline,
.encrypt_func = &_encrypt_func_trampoline,
.decrypt_func = &_decrypt_func_trampoline,
.checksum_func = &_checksum_func_trampoline,
};
esp_err_t ret = _host_init();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s initialise host failed: %s", __func__, esp_err_to_name(ret));
return ret;
}
ret = esp_blufi_register_callbacks(&blufi_callbacks);
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s blufi register failed, error code = %x", __func__, ret);
return ret;
}
ret = _gap_register_callback();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s gap register failed, error code = %x", __func__, ret);
return ret;
}
return ESP_OK;
}
#endif /* CONFIG_BT_BLUEDROID_ENABLED */
#ifdef CONFIG_BT_NIMBLE_ENABLED
// Stubs for NimBLE specific store functionality
void ble_store_config_init();
void Blufi::_nimble_on_reset(int reason) {
ESP_LOGE(BLUFI_TAG, "NimBLE Resetting state; reason=%d", reason);
}
void Blufi::_nimble_on_sync() { esp_blufi_profile_init(); }
void Blufi::_nimble_host_task(void* param) {
ESP_LOGI(BLUFI_TAG, "BLE Host Task Started");
nimble_port_run();
nimble_port_freertos_deinit();
}
esp_err_t Blufi::_host_init() {
ble_hs_cfg.reset_cb = _nimble_on_reset;
ble_hs_cfg.sync_cb = _nimble_on_sync;
ble_hs_cfg.gatts_register_cb = esp_blufi_gatt_svr_register_cb;
ble_hs_cfg.sm_io_cap = 4;
#ifdef CONFIG_EXAMPLE_BONDING
ble_hs_cfg.sm_bonding = 1;
#endif
int rc = esp_blufi_gatt_svr_init();
assert(rc == 0);
ble_store_config_init();
esp_blufi_btc_init();
esp_err_t err = esp_nimble_enable(_nimble_host_task);
if (err) {
ESP_LOGE(BLUFI_TAG, "%s failed: %s", __func__, esp_err_to_name(err));
return ESP_FAIL;
}
return ESP_OK;
}
esp_err_t Blufi::_host_deinit(void) {
esp_err_t ret = nimble_port_stop();
if (ret == ESP_OK) {
esp_nimble_deinit();
}
esp_blufi_gatt_svr_deinit();
ret = esp_blufi_profile_deinit();
esp_blufi_btc_deinit();
return ret;
}
esp_err_t Blufi::_gap_register_callback(void) { return ESP_OK; }
esp_err_t Blufi::_host_and_cb_init() {
static esp_blufi_callbacks_t blufi_callbacks = {
.event_cb = &_event_callback_trampoline,
.negotiate_data_handler = &_negotiate_data_handler_trampoline,
.encrypt_func = &_encrypt_func_trampoline,
.decrypt_func = &_decrypt_func_trampoline,
.checksum_func = &_checksum_func_trampoline,
};
esp_err_t ret = esp_blufi_register_callbacks(&blufi_callbacks);
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s blufi register failed, error code = %x", __func__, ret);
return ret;
}
// Host init must be called after registering callbacks for NimBLE
ret = _host_init();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s initialise host failed: %s", __func__, esp_err_to_name(ret));
return ret;
}
return ESP_OK;
}
#endif /* CONFIG_BT_NIMBLE_ENABLED */
#if CONFIG_BT_CONTROLLER_ENABLED || !CONFIG_BT_NIMBLE_ENABLED
esp_err_t Blufi::_controller_init() {
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
esp_err_t ret = esp_bt_controller_init(&bt_cfg);
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s initialize controller failed: %s", __func__, esp_err_to_name(ret));
return ret;
}
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
return ret;
}
#ifdef CONFIG_BT_NIMBLE_ENABLED
ret = esp_nimble_init();
if (ret) {
ESP_LOGE(BLUFI_TAG, "esp_nimble_init() failed: %s", esp_err_to_name(ret));
return ret;
}
#endif
return ESP_OK;
}
esp_err_t Blufi::_controller_deinit() {
esp_err_t ret = esp_bt_controller_disable();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s disable controller failed: %s", __func__, esp_err_to_name(ret));
}
ret = esp_bt_controller_deinit();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s deinit controller failed: %s", __func__, esp_err_to_name(ret));
}
return ret;
}
#endif
static int myrand(void* rng_state, unsigned char* output, size_t len) {
esp_fill_random(output, len);
return 0;
}
void Blufi::_security_init() {
m_sec = new BlufiSecurity();
if (m_sec == nullptr) {
ESP_LOGE(BLUFI_TAG, "Failed to allocate security context");
return;
}
memset(m_sec, 0, sizeof(BlufiSecurity));
m_sec->dhm = new mbedtls_dhm_context();
m_sec->aes = new mbedtls_aes_context();
mbedtls_dhm_init(m_sec->dhm);
mbedtls_aes_init(m_sec->aes);
memset(m_sec->iv, 0x0, sizeof(m_sec->iv));
}
void Blufi::_security_deinit() {
if (m_sec == nullptr)
return;
if (m_sec->dh_param) {
free(m_sec->dh_param);
}
mbedtls_dhm_free(m_sec->dhm);
mbedtls_aes_free(m_sec->aes);
delete m_sec->dhm;
delete m_sec->aes;
delete m_sec;
m_sec = nullptr;
}
void Blufi::_dh_negotiate_data_handler(uint8_t* data, int len, uint8_t** output_data,
int* output_len, bool* need_free) {
if (m_sec == nullptr) {
ESP_LOGE(BLUFI_TAG, "Security not initialized in DH handler");
btc_blufi_report_error(ESP_BLUFI_INIT_SECURITY_ERROR);
return;
}
if (len < 1) {
ESP_LOGE(BLUFI_TAG, "DH handler: data too short");
btc_blufi_report_error(ESP_BLUFI_DATA_FORMAT_ERROR);
return;
}
uint8_t type = data[0];
switch (type) {
case 0x00:
if (len < 3) {
ESP_LOGE(BLUFI_TAG, "DH_PARAM_LEN packet too short");
btc_blufi_report_error(ESP_BLUFI_DATA_FORMAT_ERROR);
return;
}
m_sec->dh_param_len = (data[1] << 8) | data[2];
if (m_sec->dh_param) {
free(m_sec->dh_param);
m_sec->dh_param = nullptr;
}
m_sec->dh_param = (uint8_t*)malloc(m_sec->dh_param_len);
if (m_sec->dh_param == nullptr) {
ESP_LOGE(BLUFI_TAG, "DH malloc failed");
btc_blufi_report_error(ESP_BLUFI_DH_MALLOC_ERROR);
}
break;
case 0x01: {
if (m_sec->dh_param == nullptr) {
ESP_LOGE(BLUFI_TAG, "DH param not allocated");
btc_blufi_report_error(ESP_BLUFI_DH_PARAM_ERROR);
return;
}
uint8_t* param = m_sec->dh_param;
memcpy(m_sec->dh_param, &data[1], m_sec->dh_param_len);
int ret = mbedtls_dhm_read_params(m_sec->dhm, &param, &param[m_sec->dh_param_len]);
if (ret) {
ESP_LOGE(BLUFI_TAG, "mbedtls_dhm_read_params failed %d", ret);
btc_blufi_report_error(ESP_BLUFI_READ_PARAM_ERROR);
return;
}
const int dhm_len = mbedtls_dhm_get_len(m_sec->dhm);
ret = mbedtls_dhm_make_public(m_sec->dhm, dhm_len, m_sec->self_public_key, dhm_len,
myrand, NULL);
if (ret != 0) {
ESP_LOGE(BLUFI_TAG, "mbedtls_dhm_make_public failed: %d", ret);
btc_blufi_report_error(ESP_BLUFI_MAKE_PUBLIC_ERROR);
return;
}
ret = mbedtls_dhm_calc_secret(m_sec->dhm, m_sec->share_key, SHARE_KEY_LEN,
&m_sec->share_len, myrand, NULL);
if (ret != 0) {
ESP_LOGE(BLUFI_TAG, "mbedtls_dhm_calc_secret failed: %d", ret);
btc_blufi_report_error(ESP_BLUFI_ENCRYPT_ERROR);
return;
}
ret = mbedtls_md5(m_sec->share_key, m_sec->share_len, m_sec->psk);
if (ret != 0) {
ESP_LOGE(BLUFI_TAG, "mbedtls_md5 failed: %d", ret);
btc_blufi_report_error(ESP_BLUFI_CALC_MD5_ERROR);
return;
}
ret = mbedtls_aes_setkey_enc(m_sec->aes, m_sec->psk, PSK_LEN * 8);
if (ret != 0) {
ESP_LOGE(BLUFI_TAG, "mbedtls_aes_setkey_enc failed: -0x%04X", -ret);
btc_blufi_report_error(ESP_BLUFI_ENCRYPT_ERROR);
return;
}
*output_data = m_sec->self_public_key;
*output_len = dhm_len;
*need_free = false;
ESP_LOGI(BLUFI_TAG, "DH negotiation completed successfully");
free(m_sec->dh_param);
m_sec->dh_param = nullptr;
m_sec->dh_param_len = 0;
break;
}
default:
ESP_LOGE(BLUFI_TAG, "DH handler unknown type: %d", type);
}
}
int Blufi::_aes_encrypt(uint8_t iv8, uint8_t* crypt_data, int crypt_len) {
if (!m_sec || !m_sec->aes || !crypt_data || crypt_len <= 0) {
ESP_LOGE(BLUFI_TAG, "Invalid parameters for AES encryption");
return -ESP_ERR_INVALID_ARG;
}
size_t iv_offset = 0;
uint8_t iv0[16];
memcpy(iv0, m_sec->iv, 16);
iv0[0] = iv8;
int ret = mbedtls_aes_crypt_cfb128(m_sec->aes, MBEDTLS_AES_ENCRYPT, crypt_len, &iv_offset, iv0,
crypt_data, crypt_data);
if (ret == 0) {
return crypt_len;
} else {
ESP_LOGE(BLUFI_TAG, "AES encrypt failed: %d", ret);
return ret;
}
}
int Blufi::_aes_decrypt(uint8_t iv8, uint8_t* crypt_data, int crypt_len) {
if (!m_sec || !m_sec->aes || !crypt_data || crypt_len < 0) {
ESP_LOGE(BLUFI_TAG, "Invalid parameters for AES decryption %p %p %d", m_sec->aes,
crypt_data, crypt_len);
return -ESP_ERR_INVALID_ARG;
}
size_t iv_offset = 0;
uint8_t iv0[16];
memcpy(iv0, m_sec->iv, 16);
iv0[0] = iv8;
int ret = mbedtls_aes_crypt_cfb128(m_sec->aes, MBEDTLS_AES_DECRYPT, crypt_len, &iv_offset, iv0,
crypt_data, crypt_data);
if (ret != 0) {
ESP_LOGE(BLUFI_TAG, "AES decrypt failed: %d", ret);
return ret;
} else {
return crypt_len;
}
}
uint16_t Blufi::_crc_checksum(uint8_t iv8, uint8_t* data, int len) {
return esp_crc16_be(0, data, len);
}
int Blufi::_get_softap_conn_num() {
auto& wifi = WifiManager::GetInstance();
if (!wifi.IsInitialized() || !wifi.IsConfigMode()) {
return 0;
}
wifi_sta_list_t sta_list{};
if (esp_wifi_ap_get_sta_list(&sta_list) == ESP_OK) {
return sta_list.num;
}
return 0;
}
void Blufi::start_wifi_scan() {
ESP_LOGI(BLUFI_TAG, "Starting dedicated WiFi scan");
// Check if a scan is already in progress
if (m_scan_in_progress) {
ESP_LOGW(BLUFI_TAG, "Scan already in progress, skipping");
return;
}
m_scan_in_progress = true;
// Get current WiFi mode
wifi_mode_t current_mode;
esp_err_t err = esp_wifi_get_mode(&current_mode);
if (current_mode == WIFI_MODE_AP) {
// If in AP mode, temporarily switch to APSTA to allow scanning
ESP_LOGI(BLUFI_TAG, "WiFi in AP mode");
err = esp_wifi_set_mode(WIFI_MODE_STA);
if (err != ESP_OK) {
ESP_LOGE(BLUFI_TAG, "Failed to set WiFi mode to STA: %s", esp_err_to_name(err));
m_scan_in_progress = false;
return;
}
// Need to restart WiFi for mode change to take effect
err = esp_wifi_start();
if (err != ESP_OK) {
ESP_LOGE(BLUFI_TAG, "Failed to start WiFi after mode switch: %s", esp_err_to_name(err));
m_scan_in_progress = false;
return;
}
// Register scan event handler
esp_event_handler_instance_t scan_event_instance;
esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&Blufi::_wifi_scan_event_handler, this,
&scan_event_instance);
// Start scan
err = esp_wifi_scan_start(NULL, false);
if (err != ESP_OK) {
ESP_LOGE(BLUFI_TAG, "Failed to start WiFi scan: %s", esp_err_to_name(err));
m_scan_in_progress = false;
return;
}
} else if (current_mode == WIFI_MODE_STA) {
// Start scan
err = esp_wifi_scan_start(NULL, false);
if (err != ESP_OK) {
ESP_LOGE(BLUFI_TAG, "Failed to start WiFi scan: %s", esp_err_to_name(err));
m_scan_in_progress = false;
return;
}
} else {
ESP_LOGE(BLUFI_TAG, "Unexpected WiFi mode: %d", current_mode);
m_scan_in_progress = false;
return;
}
ESP_LOGI(BLUFI_TAG, "WiFi scan started");
}
void Blufi::_send_wifi_list() {
if (m_ap_records.empty()) {
ESP_LOGW(BLUFI_TAG, "No AP records available to send");
return;
}
ESP_LOGI(BLUFI_TAG, "Sending WiFi list with %d APs", m_ap_records.size());
std::vector<esp_blufi_ap_record_t> blufi_ap_list;
for (const auto& ap : m_ap_records) {
esp_blufi_ap_record_t blufi_ap;
memset(&blufi_ap, 0, sizeof(blufi_ap));
memcpy(blufi_ap.ssid, ap.ssid, std::min((size_t)32, sizeof(ap.ssid)));
blufi_ap.rssi = ap.rssi;
blufi_ap_list.push_back(blufi_ap);
}
esp_blufi_send_wifi_list(blufi_ap_list.size(), blufi_ap_list.data());
m_ap_records.clear();
start_wifi_scan();
}
void Blufi::_wifi_scan_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id,
void* event_data) {
Blufi* self = static_cast<Blufi*>(arg);
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) {
ESP_LOGI(BLUFI_TAG, "WiFi scan done");
uint16_t ap_num = 0;
esp_wifi_scan_get_ap_num(&ap_num);
if (ap_num == 0) {
ESP_LOGW(BLUFI_TAG, "No APs found");
self->m_ap_records.clear();
} else {
if (static_cast<Blufi*>(arg)->m_scan_should_save_ssid == true) {
self->m_ap_records.resize(ap_num);
esp_wifi_scan_get_ap_records(&ap_num, self->m_ap_records.data());
ESP_LOGI(BLUFI_TAG, "Found %d APs", ap_num);
for (const auto& ap : self->m_ap_records) {
ESP_LOGI(BLUFI_TAG, " SSID: %s, RSSI: %d, Authmode: %d", (char*)ap.ssid,
ap.rssi, ap.authmode);
}
}
}
self->m_scan_in_progress = false;
}
}
void Blufi::_handle_event(esp_blufi_cb_event_t event, esp_blufi_cb_param_t* param) {
switch (event) {
case ESP_BLUFI_EVENT_INIT_FINISH:
ESP_LOGI(BLUFI_TAG, "BLUFI init finish");
esp_ble_gap_set_device_name(BLUFI_DEVICE_NAME);
esp_blufi_adv_start();
break;
case ESP_BLUFI_EVENT_DEINIT_FINISH:
ESP_LOGI(BLUFI_TAG, "BLUFI deinit finish");
break;
case ESP_BLUFI_EVENT_BLE_CONNECT:
ESP_LOGI(BLUFI_TAG, "BLUFI ble connect");
m_ble_is_connected = true;
esp_blufi_adv_stop();
_security_init();
break;
case ESP_BLUFI_EVENT_BLE_DISCONNECT:
ESP_LOGI(BLUFI_TAG, "BLUFI ble disconnect");
m_ble_is_connected = false;
_security_deinit();
if (!m_provisioned) {
esp_blufi_adv_start();
} else {
esp_blufi_adv_stop();
if (!m_deinited) {
xTaskCreate(
[](void* ctx) {
static_cast<Blufi*>(ctx)->deinit();
vTaskDelete(nullptr);
},
"blufi_deinit", 4096, this, 5, nullptr);
}
}
break;
case ESP_BLUFI_EVENT_SET_WIFI_OPMODE: {
ESP_LOGI(BLUFI_TAG, "BLUFI Set WIFI opmode %d", param->wifi_mode.op_mode);
auto& wifi_manager = WifiManager::GetInstance();
if (!wifi_manager.IsInitialized() && !wifi_manager.Initialize()) {
ESP_LOGE(BLUFI_TAG, "Failed to initialize WifiManager for opmode change");
break;
}
switch (param->wifi_mode.op_mode) {
case WIFI_MODE_STA:
wifi_manager.StartStation();
break;
case WIFI_MODE_AP:
wifi_manager.StartConfigAp();
break;
case WIFI_MODE_APSTA:
ESP_LOGW(BLUFI_TAG, "APSTA mode not supported, starting station only");
wifi_manager.StartStation();
break;
default:
wifi_manager.StopStation();
wifi_manager.StopConfigAp();
break;
}
break;
}
case ESP_BLUFI_EVENT_REQ_CONNECT_TO_AP: {
ESP_LOGI(BLUFI_TAG, "BLUFI request wifi connect to AP via esp-wifi-connect");
std::string ssid(reinterpret_cast<const char*>(m_sta_config.sta.ssid));
std::string password(reinterpret_cast<const char*>(m_sta_config.sta.password));
SsidManager::GetInstance().AddSsid(ssid, password);
m_scan_should_save_ssid = false;
m_sta_ssid_len = static_cast<int>(std::min(ssid.size(), sizeof(m_sta_ssid)));
memcpy(m_sta_ssid, ssid.c_str(), m_sta_ssid_len);
memset(m_sta_bssid, 0, sizeof(m_sta_bssid));
m_sta_connected = false;
m_sta_got_ip = false;
m_sta_is_connecting = true;
m_sta_conn_info = {};
m_sta_conn_info.sta_ssid = m_sta_ssid;
m_sta_conn_info.sta_ssid_len = m_sta_ssid_len;
auto& wifi_manager = WifiManager::GetInstance();
if (wifi_manager.IsInitialized()) {
if (wifi_manager.IsConfigMode()) {
wifi_manager.StopConfigAp();
}
wifi_manager.StopStation();
}
if (!wifi_manager.IsInitialized() && !wifi_manager.Initialize()) {
ESP_LOGE(BLUFI_TAG, "Failed to initialize WifiManager");
break;
}
vTaskDelay(pdMS_TO_TICKS(500));
wifi_manager.StartStation();
xTaskCreate(
[](void* ctx) {
auto* self = static_cast<Blufi*>(ctx);
auto& wifi = WifiManager::GetInstance();
constexpr int kConnectTimeoutMs = 10000;
constexpr TickType_t kDelayTick = pdMS_TO_TICKS(200);
int waited_ms = 0;
while (waited_ms < kConnectTimeoutMs && !wifi.IsConnected()) {
vTaskDelay(kDelayTick);
waited_ms += 200;
}
wifi_mode_t mode = GetWifiModeWithFallback(wifi);
const int softap_conn_num = _get_softap_conn_num();
if (wifi.IsConnected()) {
self->m_sta_is_connecting = false;
self->m_sta_connected = true;
self->m_sta_got_ip = true;
self->m_provisioned = true;
auto current_ssid = wifi.GetSsid();
if (!current_ssid.empty()) {
self->m_sta_ssid_len = static_cast<int>(
std::min(current_ssid.size(), sizeof(self->m_sta_ssid)));
memcpy(self->m_sta_ssid, current_ssid.c_str(), self->m_sta_ssid_len);
}
wifi_ap_record_t ap_info{};
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
memcpy(self->m_sta_bssid, ap_info.bssid, sizeof(self->m_sta_bssid));
}
esp_blufi_extra_info_t info = {};
memcpy(info.sta_bssid, self->m_sta_bssid, sizeof(self->m_sta_bssid));
info.sta_bssid_set = true;
info.sta_ssid = self->m_sta_ssid;
info.sta_ssid_len = self->m_sta_ssid_len;
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONN_SUCCESS,
softap_conn_num, &info);
ESP_LOGI(BLUFI_TAG, "connected to WiFi");
if (self->m_ble_is_connected) {
esp_blufi_disconnect();
}
} else {
self->m_sta_is_connecting = false;
self->m_sta_connected = false;
self->m_sta_got_ip = false;
esp_blufi_extra_info_t info = {};
info.sta_ssid = self->m_sta_ssid;
info.sta_ssid_len = self->m_sta_ssid_len;
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONN_FAIL,
softap_conn_num, &info);
ESP_LOGE(BLUFI_TAG, "Failed to connect to WiFi via esp-wifi-connect");
}
vTaskDelete(nullptr);
},
"blufi_wifi_conn", 4096, this, 5, nullptr);
break;
}
case ESP_BLUFI_EVENT_REQ_DISCONNECT_FROM_AP:
ESP_LOGI(BLUFI_TAG, "BLUFI request wifi disconnect from AP");
if (WifiManager::GetInstance().IsInitialized()) {
WifiManager::GetInstance().StopStation();
}
m_sta_is_connecting = false;
m_sta_connected = false;
m_sta_got_ip = false;
break;
case ESP_BLUFI_EVENT_GET_WIFI_STATUS: {
auto& wifi = WifiManager::GetInstance();
wifi_mode_t mode = GetWifiModeWithFallback(wifi);
const int softap_conn_num = _get_softap_conn_num();
if (wifi.IsInitialized() && wifi.IsConnected()) {
m_sta_connected = true;
m_sta_got_ip = true;
auto current_ssid = wifi.GetSsid();
if (!current_ssid.empty()) {
m_sta_ssid_len =
static_cast<int>(std::min(current_ssid.size(), sizeof(m_sta_ssid)));
memcpy(m_sta_ssid, current_ssid.c_str(), m_sta_ssid_len);
}
esp_blufi_extra_info_t info;
memset(&info, 0, sizeof(esp_blufi_extra_info_t));
memcpy(info.sta_bssid, m_sta_bssid, 6);
info.sta_ssid = m_sta_ssid;
info.sta_ssid_len = m_sta_ssid_len;
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONN_SUCCESS, softap_conn_num,
&info);
} else if (m_sta_is_connecting) {
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONNECTING, softap_conn_num,
&m_sta_conn_info);
} else {
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONN_FAIL, softap_conn_num,
&m_sta_conn_info);
}
ESP_LOGI(BLUFI_TAG, "BLUFI get wifi status");
break;
}
case ESP_BLUFI_EVENT_RECV_STA_BSSID:
memcpy(m_sta_config.sta.bssid, param->sta_bssid.bssid, 6);
m_sta_config.sta.bssid_set = true;
ESP_LOGI(BLUFI_TAG, "Recv STA BSSID");
break;
case ESP_BLUFI_EVENT_RECV_STA_SSID:
strncpy((char*)m_sta_config.sta.ssid, (char*)param->sta_ssid.ssid,
param->sta_ssid.ssid_len);
m_sta_config.sta.ssid[param->sta_ssid.ssid_len] = '\0';
ESP_LOGI(BLUFI_TAG, "Recv STA SSID: %s", m_sta_config.sta.ssid);
break;
case ESP_BLUFI_EVENT_RECV_STA_PASSWD:
strncpy((char*)m_sta_config.sta.password, (char*)param->sta_passwd.passwd,
param->sta_passwd.passwd_len);
m_sta_config.sta.password[param->sta_passwd.passwd_len] = '\0';
ESP_LOGI(BLUFI_TAG, "Recv STA PASSWORD : %s", m_sta_config.sta.password);
break;
case ESP_BLUFI_EVENT_GET_WIFI_LIST: {
ESP_LOGI(BLUFI_TAG, "BLUFI get wifi list");
while (m_scan_in_progress) {
vTaskDelay(pdMS_TO_TICKS(500));
}
_send_wifi_list();
break;
}
default:
ESP_LOGW(BLUFI_TAG, "Unhandled event: %d", event);
break;
}
}
void Blufi::_event_callback_trampoline(esp_blufi_cb_event_t event, esp_blufi_cb_param_t* param) {
GetInstance()._handle_event(event, param);
}
void Blufi::_negotiate_data_handler_trampoline(uint8_t* data, int len, uint8_t** output_data,
int* output_len, bool* need_free) {
GetInstance()._dh_negotiate_data_handler(data, len, output_data, output_len, need_free);
}
int Blufi::_encrypt_func_trampoline(uint8_t iv8, uint8_t* crypt_data, int crypt_len) {
return GetInstance()._aes_encrypt(iv8, crypt_data, crypt_len);
}
int Blufi::_decrypt_func_trampoline(uint8_t iv8, uint8_t* crypt_data, int crypt_len) {
return GetInstance()._aes_decrypt(iv8, crypt_data, crypt_len);
}
uint16_t Blufi::_checksum_func_trampoline(uint8_t iv8, uint8_t* data, int len) {
return _crc_checksum(iv8, data, len);
}

147
main/boards/common/blufi.h Normal file
View File

@@ -0,0 +1,147 @@
#pragma once
#include <aes/esp_aes.h>
#include <cassert>
#include <cstring>
#include <vector>
#include "esp_blufi_api.h"
#include "esp_err.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mbedtls/aes.h"
#include "mbedtls/dhm.h"
#include "wifi_manager.h"
class Blufi {
public:
/**
* @brief Get the singleton instance of the Blufi class.
*/
static Blufi &GetInstance();
/**
* @brief Start WiFi scan for Blufi provisioning
* This method intelligently handles WiFi scanning based on current WiFi state:
* - If WiFi config mode is active, it uses the existing scan results from WifiConfigurationAp
* - Otherwise, it performs a dedicated scan without interfering with normal WiFi operations
*/
void start_wifi_scan();
/**
* @brief Initializes the Bluetooth controller, host, and Blufi profile.
* This is the main entry point to start the Blufi process.
* @return ESP_OK on success, otherwise an error code.
*/
esp_err_t init();
/**
* @brief Deinitializes Blufi and the Bluetooth stack.
* @return ESP_OK on success, otherwise an error code.
*/
esp_err_t deinit();
// Delete copy constructor and assignment operator for singleton
Blufi(const Blufi &) = delete;
Blufi &operator=(const Blufi &) = delete;
private:
bool inited_ = false;
Blufi();
~Blufi();
// Initialization logic
static esp_err_t _controller_init();
static esp_err_t _controller_deinit();
static esp_err_t _host_init();
static esp_err_t _host_deinit();
static esp_err_t _gap_register_callback();
static esp_err_t _host_and_cb_init();
void _security_init();
void _security_deinit();
void _dh_negotiate_data_handler(uint8_t *data, int len, uint8_t **output_data, int *output_len,
bool *need_free);
int _aes_encrypt(uint8_t iv8, uint8_t *crypt_data, int crypt_len);
int _aes_decrypt(uint8_t iv8, uint8_t *crypt_data, int crypt_len);
static uint16_t _crc_checksum(uint8_t iv8, uint8_t *data, int len);
void _handle_event(esp_blufi_cb_event_t event, esp_blufi_cb_param_t *param);
static int _get_softap_conn_num();
// WiFi scan methods
void _send_wifi_list();
void _start_dedicated_wifi_scan();
static void _wifi_scan_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data);
// These C-style functions are registered with ESP-IDF and call the corresponding instance
// methods.
static void _event_callback_trampoline(esp_blufi_cb_event_t event, esp_blufi_cb_param_t *param);
static void _negotiate_data_handler_trampoline(uint8_t *data, int len, uint8_t **output_data,
int *output_len, bool *need_free);
static int _encrypt_func_trampoline(uint8_t iv8, uint8_t *crypt_data, int crypt_len);
static int _decrypt_func_trampoline(uint8_t iv8, uint8_t *crypt_data, int crypt_len);
static uint16_t _checksum_func_trampoline(uint8_t iv8, uint8_t *data, int len);
#ifdef CONFIG_BT_NIMBLE_ENABLED
static void _nimble_on_reset(int reason);
static void _nimble_on_sync();
static void _nimble_host_task(void *param);
#endif
// Security context, formerly blufi_sec struct
struct BlufiSecurity {
#define DH_SELF_PUB_KEY_LEN 128
uint8_t self_public_key[DH_SELF_PUB_KEY_LEN];
#define SHARE_KEY_LEN 128
uint8_t share_key[SHARE_KEY_LEN];
size_t share_len;
#define PSK_LEN 16
uint8_t psk[PSK_LEN];
uint8_t *dh_param;
int dh_param_len;
uint8_t iv[16];
mbedtls_dhm_context *dhm;
esp_aes_context *aes;
};
BlufiSecurity *m_sec;
// State variables
wifi_config_t m_sta_config{};
bool m_ble_is_connected;
bool m_sta_connected;
bool m_sta_got_ip;
bool m_provisioned;
bool m_deinited;
uint8_t m_sta_bssid[6]{};
uint8_t m_sta_ssid[32]{};
int m_sta_ssid_len;
bool m_sta_is_connecting;
esp_blufi_extra_info_t m_sta_conn_info{};
// WiFi scan related
std::vector<wifi_ap_record_t> m_ap_records;
bool m_scan_in_progress = false;
bool m_scan_should_save_ssid = true;
};

178
main/boards/common/board.cc Normal file
View File

@@ -0,0 +1,178 @@
#include "board.h"
#include "system_info.h"
#include "settings.h"
#include "display/display.h"
#include "display/oled_display.h"
#include "assets/lang_config.h"
#include <esp_log.h>
#include <esp_ota_ops.h>
#include <esp_chip_info.h>
#include <esp_random.h>
#define TAG "Board"
Board::Board() {
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);
}
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;
}
Led* Board::GetLed() {
static NoLed led;
return &led;
}
std::string Board::GetSystemInfoJson() {
/*
{
"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"(},)";
// Append display info
auto display = GetDisplay();
if (display) {
json += R"("display":{)";
if (dynamic_cast<OledDisplay*>(display)) {
json += R"("monochrome":)" + std::string("true") + R"(,)";
} else {
json += R"("monochrome":)" + std::string("false") + R"(,)";
}
json += R"("width":)" + std::to_string(display->width()) + R"(,)";
json += R"("height":)" + std::to_string(display->height()) + R"(,)";
json.pop_back(); // Remove the last comma
}
json += R"(},)";
json += R"("board":)" + GetBoardJson();
// Close the JSON object
json += R"(})";
return json;
}

View File

@@ -0,0 +1,92 @@
#ifndef BOARD_H
#define BOARD_H
#include <http.h>
#include <web_socket.h>
#include <mqtt.h>
#include <udp.h>
#include <string>
#include <functional>
#include <network_interface.h>
#include "led/led.h"
#include "backlight.h"
#include "camera.h"
#include "assets.h"
/**
* Network events for unified callback
*/
enum class NetworkEvent {
Scanning, // Network is scanning (WiFi scanning, etc.)
Connecting, // Network is connecting (data: SSID/network name)
Connected, // Network connected successfully (data: SSID/network name)
Disconnected, // Network disconnected
WifiConfigModeEnter, // Entered WiFi configuration mode
WifiConfigModeExit, // Exited WiFi configuration mode
// Cellular modem specific events
ModemDetecting, // Detecting modem (baud rate, module type)
ModemErrorNoSim, // No SIM card detected
ModemErrorRegDenied, // Network registration denied
ModemErrorInitFailed, // Modem initialization failed
ModemErrorTimeout // Operation timeout
};
// Power save level enumeration
enum class PowerSaveLevel {
LOW_POWER, // Maximum power saving (lowest power consumption)
BALANCED, // Medium power saving (balanced)
PERFORMANCE, // No power saving (maximum power consumption / full performance)
};
// Network event callback type (event, data)
// data contains additional info like SSID for Connecting/Connected events
using NetworkEventCallback = std::function<void(NetworkEvent event, const std::string& data)>;
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_;
public:
static Board& GetInstance() {
static Board* instance = static_cast<Board*>(create_board());
return *instance;
}
virtual ~Board() = default;
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 NetworkInterface* GetNetwork() = 0;
virtual void StartNetwork() = 0;
virtual void SetNetworkEventCallback(NetworkEventCallback callback) { (void)callback; }
virtual const char* GetNetworkStateIcon() = 0;
virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging);
virtual std::string GetSystemInfoJson();
virtual void SetPowerSaveLevel(PowerSaveLevel level) = 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

View 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);
}

View 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_

View File

@@ -0,0 +1,16 @@
#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 bool SetSwapBytes(bool enabled) { return false; } // Optional, default no-op
virtual std::string Explain(const std::string& question) = 0;
};
#endif // CAMERA_H

View File

@@ -0,0 +1,98 @@
#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();
}
void DualNetworkBoard::SetNetworkEventCallback(NetworkEventCallback callback) {
// Forward the callback to the current board
current_board_->SetNetworkEventCallback(std::move(callback));
}
NetworkInterface* DualNetworkBoard::GetNetwork() {
return current_board_->GetNetwork();
}
const char* DualNetworkBoard::GetNetworkStateIcon() {
return current_board_->GetNetworkStateIcon();
}
void DualNetworkBoard::SetPowerSaveLevel(PowerSaveLevel level) {
current_board_->SetPowerSaveLevel(level);
}
std::string DualNetworkBoard::GetBoardJson() {
return current_board_->GetBoardJson();
}
std::string DualNetworkBoard::GetDeviceStatusJson() {
return current_board_->GetDeviceStatusJson();
}

View File

@@ -0,0 +1,60 @@
#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 void SetNetworkEventCallback(NetworkEventCallback callback) override;
virtual NetworkInterface* GetNetwork() override;
virtual const char* GetNetworkStateIcon() override;
virtual void SetPowerSaveLevel(PowerSaveLevel level) override;
virtual std::string GetBoardJson() override;
virtual std::string GetDeviceStatusJson() override;
};
#endif // DUAL_NETWORK_BOARD_H

View File

@@ -0,0 +1,322 @@
#include "sdkconfig.h"
#include <esp_heap_caps.h>
#include <cstdio>
#include <cstring>
#include <esp_log.h>
#include <img_converters.h>
#include "esp32_camera.h"
#include "board.h"
#include "display.h"
#include "lvgl_display.h"
#include "mcp_server.h"
#include "system_info.h"
#include "jpg/image_to_jpeg.h"
#include "esp_timer.h"
#define TAG "Esp32Camera"
Esp32Camera::Esp32Camera(const camera_config_t &config) {
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_camera_init failed with error 0x%x", err);
return;
}
sensor_t *s = esp_camera_sensor_get();
if (s) {
if (s->id.PID == GC0308_PID) {
s->set_hmirror(s, 0); // Control camera mirror: 1 for mirror, 0 for normal
}
ESP_LOGI(TAG, "Camera initialized: format=%d", config.pixel_format);
}
streaming_on_ = true;
}
Esp32Camera::~Esp32Camera() {
if (streaming_on_) {
if (current_fb_) {
esp_camera_fb_return(current_fb_);
current_fb_ = nullptr;
}
if (encode_buf_) {
heap_caps_free(encode_buf_);
encode_buf_ = nullptr;
encode_buf_size_ = 0;
}
esp_camera_deinit();
streaming_on_ = false;
}
}
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();
}
if (!streaming_on_) {
return false;
}
// Get the latest frame, discard old frames for real-time performance
for (int i = 0; i < 2; i++) {
if (current_fb_) {
esp_camera_fb_return(current_fb_);
}
current_fb_ = esp_camera_fb_get();
if (!current_fb_) {
ESP_LOGE(TAG, "Camera capture failed");
return false;
}
}
// Prepare encode buffer for RGB565 format (with optional byte swapping)
if (current_fb_->format == PIXFORMAT_RGB565) {
size_t pixel_count = current_fb_->width * current_fb_->height;
size_t data_size = pixel_count * 2;
// Allocate or reallocate encode buffer if needed
if (encode_buf_size_ < data_size) {
if (encode_buf_) {
heap_caps_free(encode_buf_);
}
encode_buf_ = (uint8_t *)heap_caps_malloc(data_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (encode_buf_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate memory for encode buffer");
encode_buf_size_ = 0;
return false;
}
encode_buf_size_ = data_size;
}
// Copy data to encode buffer with optional byte swapping
uint16_t *src = (uint16_t *)current_fb_->buf;
uint16_t *dst = (uint16_t *)encode_buf_;
if (swap_bytes_enabled_) {
for (size_t i = 0; i < pixel_count; i++) {
dst[i] = __builtin_bswap16(src[i]);
}
} else {
memcpy(encode_buf_, current_fb_->buf, data_size);
}
// Allocate separate buffer for preview display
uint8_t *preview_data = (uint8_t *)heap_caps_malloc(data_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (preview_data != nullptr) {
memcpy(preview_data, encode_buf_, data_size);
auto display = dynamic_cast<LvglDisplay *>(Board::GetInstance().GetDisplay());
if (display != nullptr) {
display->SetPreviewImage(std::make_unique<LvglAllocatedImage>(preview_data, data_size, current_fb_->width, current_fb_->height, current_fb_->width * 2, LV_COLOR_FORMAT_RGB565));
} else {
heap_caps_free(preview_data);
}
}
} else if (current_fb_->format == PIXFORMAT_JPEG) {
// JPEG format preview usually requires decoding, skip preview display for now, just log
ESP_LOGW(TAG, "JPEG capture success, len=%zu, but not supported for preview", current_fb_->len);
}
ESP_LOGI(TAG, "Captured frame: %dx%d, len=%zu, format=%d",
current_fb_->width, current_fb_->height, current_fb_->len, current_fb_->format);
return true;
}
bool Esp32Camera::SetHMirror(bool enabled) {
sensor_t *s = esp_camera_sensor_get();
if (!s) {
return false;
}
s->set_hmirror(s, enabled ? 1 : 0);
return true;
}
bool Esp32Camera::SetVFlip(bool enabled) {
sensor_t *s = esp_camera_sensor_get();
if (!s) {
return false;
}
s->set_vflip(s, enabled ? 1 : 0);
return true;
}
bool Esp32Camera::SetSwapBytes(bool enabled) {
swap_bytes_enabled_ = enabled;
return true;
}
std::string Esp32Camera::Explain(const std::string &question) {
if (explain_url_.empty()) {
throw std::runtime_error("Image explain URL or token is not set");
}
if (current_fb_ == nullptr) {
throw std::runtime_error("No camera frame captured");
}
// Create local JPEG queue
QueueHandle_t jpeg_queue = xQueueCreate(40, sizeof(JpegChunk));
if (jpeg_queue == nullptr) {
ESP_LOGE(TAG, "Failed to create JPEG queue");
throw std::runtime_error("Failed to create JPEG queue");
}
// Start encoding thread
encoder_thread_ = std::thread([this, jpeg_queue]() {
int64_t start_time = esp_timer_get_time();
uint16_t w = current_fb_->width;
uint16_t h = current_fb_->height;
v4l2_pix_fmt_t enc_fmt;
switch (current_fb_->format) {
case PIXFORMAT_RGB565:
enc_fmt = V4L2_PIX_FMT_RGB565;
break;
case PIXFORMAT_YUV422:
enc_fmt = V4L2_PIX_FMT_YUYV; // YUV422 is actually YUYV format
break;
case PIXFORMAT_YUV420:
enc_fmt = V4L2_PIX_FMT_YUV420;
break;
case PIXFORMAT_GRAYSCALE:
enc_fmt = V4L2_PIX_FMT_GREY;
break;
case PIXFORMAT_JPEG:
enc_fmt = V4L2_PIX_FMT_JPEG;
break;
case PIXFORMAT_RGB888:
enc_fmt = V4L2_PIX_FMT_RGB24;
break;
default:
ESP_LOGE(TAG, "Unsupported pixel format: %d", current_fb_->format);
return;
}
// Use encode buffer for RGB565, otherwise use original frame buffer
uint8_t *jpeg_src_buf = current_fb_->buf;
size_t jpeg_src_len = current_fb_->len;
if (current_fb_->format == PIXFORMAT_RGB565 && encode_buf_ != nullptr) {
jpeg_src_buf = encode_buf_;
jpeg_src_len = encode_buf_size_;
}
bool ok = image_to_jpeg_cb(jpeg_src_buf, jpeg_src_len, w, h, enc_fmt, 80,
[](void* arg, size_t index, const void* data, size_t len) -> size_t {
auto jpeg_queue = static_cast<QueueHandle_t>(arg);
JpegChunk chunk = {.data = nullptr, .len = len};
if (index == 0 && data != nullptr && len > 0) {
chunk.data = (uint8_t*)heap_caps_aligned_alloc(16, len, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (chunk.data == nullptr) {
ESP_LOGE(TAG, "Failed to allocate %zu bytes for JPEG chunk", len);
chunk.len = 0;
} else {
memcpy(chunk.data, data, len);
}
} else {
chunk.len = 0; // Sentinel or error
}
xQueueSend(jpeg_queue, &chunk, portMAX_DELAY);
return len;
}, jpeg_queue);
if (!ok) {
JpegChunk chunk = {.data = nullptr, .len = 0};
xQueueSend(jpeg_queue, &chunk, portMAX_DELAY);
}
int64_t end_time = esp_timer_get_time();
ESP_LOGI(TAG, "JPEG encoding time: %ld ms", int((end_time - start_time) / 1000));
});
auto network = Board::GetInstance().GetNetwork();
auto http = network->CreateHttp(3);
std::string boundary = "----ESP32_CAMERA_BOUNDARY";
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");
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);
throw std::runtime_error("Failed to connect to explain URL");
}
{
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());
}
size_t total_sent = 0;
bool saw_terminator = false;
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) {
saw_terminator = true;
break;
}
http->Write((const char *)chunk.data, chunk.len);
total_sent += chunk.len;
heap_caps_free(chunk.data);
}
encoder_thread_.join();
vQueueDelete(jpeg_queue);
if (!saw_terminator || total_sent == 0) {
ESP_LOGE(TAG, "JPEG encoder failed or produced empty output");
throw std::runtime_error("Failed to encode image to JPEG");
}
{
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());
throw std::runtime_error("Failed to upload photo");
}
std::string result = http->ReadAll();
http->Close();
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",
current_fb_->width, current_fb_->height, (int)total_sent, (int)remain_stack_size, question.c_str(), result.c_str());
return result;
}

View File

@@ -0,0 +1,44 @@
#pragma once
#include "sdkconfig.h"
#include <lvgl.h>
#include <thread>
#include <memory>
#include <vector>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include "camera.h"
#include "esp_camera.h"
#include "jpg/image_to_jpeg.h"
struct JpegChunk
{
uint8_t *data;
size_t len;
};
class Esp32Camera : public Camera
{
private:
bool streaming_on_ = false;
bool swap_bytes_enabled_ = true; // Swap pixel byte order for RGB565, enabled by default
std::string explain_url_;
std::string explain_token_;
std::thread encoder_thread_;
camera_fb_t *current_fb_ = nullptr;
uint8_t *encode_buf_ = nullptr; // Buffer for JPEG encoding (with optional byte swap)
size_t encode_buf_size_ = 0;
public:
Esp32Camera(const camera_config_t &config);
~Esp32Camera();
virtual void SetExplainUrl(const std::string &url, const std::string &token) override;
virtual bool Capture() override;
virtual bool SetHMirror(bool enabled) override;
virtual bool SetVFlip(bool enabled) override;
virtual bool SetSwapBytes(bool enabled) override;
virtual std::string Explain(const std::string &question) override;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
#pragma once
#include "sdkconfig.h"
#include <lvgl.h>
#include <thread>
#include <memory>
#include <vector>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include "camera.h"
#include "jpg/image_to_jpeg.h"
#include "esp_video_init.h"
struct JpegChunk {
uint8_t* data;
size_t len;
};
class EspVideo : public Camera {
private:
struct FrameBuffer {
uint8_t *data = nullptr;
size_t len = 0;
uint16_t width = 0;
uint16_t height = 0;
v4l2_pix_fmt_t format = 0;
} frame_;
v4l2_pix_fmt_t sensor_format_ = 0;
#ifdef CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
uint16_t sensor_width_ = 0;
uint16_t sensor_height_ = 0;
#endif // CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
int video_fd_ = -1;
bool streaming_on_ = false;
struct MmapBuffer { void *start = nullptr; size_t length = 0; };
std::vector<MmapBuffer> mmap_buffers_;
std::string explain_url_;
std::string explain_token_;
std::thread encoder_thread_;
public:
EspVideo(const esp_video_init_config_t& config);
~EspVideo();
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);
};

View 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_, &reg, 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_, &reg, 1, buffer, length, 100));
}

View 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

View 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
View 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_

View File

@@ -0,0 +1,48 @@
#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) {
if (gpio_num_ == GPIO_NUM_NC) {
return;
}
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__

View File

@@ -0,0 +1,270 @@
#include "ml307_board.h"
#include "audio_codec.h"
#include "display.h"
#include <esp_log.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <font_awesome.h>
#include <utility>
static const char *TAG = "Ml307Board";
// Maximum retry count for modem detection
static constexpr int MODEM_DETECT_MAX_RETRIES = 30;
// Maximum retry count for network registration
static constexpr int NETWORK_REG_MAX_RETRIES = 6;
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::SetNetworkEventCallback(NetworkEventCallback callback) {
network_event_callback_ = std::move(callback);
}
void Ml307Board::OnNetworkEvent(NetworkEvent event, const std::string& data) {
switch (event) {
case NetworkEvent::ModemDetecting:
ESP_LOGI(TAG, "Detecting modem...");
break;
case NetworkEvent::Connecting:
ESP_LOGI(TAG, "Registering network...");
break;
case NetworkEvent::Connected:
ESP_LOGI(TAG, "Network connected");
break;
case NetworkEvent::Disconnected:
ESP_LOGW(TAG, "Network disconnected");
break;
case NetworkEvent::ModemErrorNoSim:
ESP_LOGE(TAG, "No SIM card detected");
break;
case NetworkEvent::ModemErrorRegDenied:
ESP_LOGE(TAG, "Network registration denied");
break;
case NetworkEvent::ModemErrorInitFailed:
ESP_LOGE(TAG, "Modem initialization failed");
break;
case NetworkEvent::ModemErrorTimeout:
ESP_LOGE(TAG, "Operation timeout");
break;
default:
break;
}
// Notify external callback if set
if (network_event_callback_) {
network_event_callback_(event, data);
}
}
void Ml307Board::NetworkTask() {
// Notify modem detection started
OnNetworkEvent(NetworkEvent::ModemDetecting);
// Try to detect modem with retry limit
int detect_retries = 0;
while (detect_retries < MODEM_DETECT_MAX_RETRIES) {
modem_ = AtModem::Detect(tx_pin_, rx_pin_, dtr_pin_, 921600);
if (modem_ != nullptr) {
break;
}
detect_retries++;
vTaskDelay(pdMS_TO_TICKS(1000));
}
if (modem_ == nullptr) {
ESP_LOGE(TAG, "Failed to detect modem after %d retries", MODEM_DETECT_MAX_RETRIES);
OnNetworkEvent(NetworkEvent::ModemErrorInitFailed);
return;
}
ESP_LOGI(TAG, "Modem detected successfully");
// Set up network state change callback
// Note: Don't call GetCarrierName() here as it sends AT command and will block ReceiveTask
modem_->OnNetworkStateChanged([this](bool network_ready) {
if (network_ready) {
OnNetworkEvent(NetworkEvent::Connected);
} else {
OnNetworkEvent(NetworkEvent::Disconnected);
}
});
// Notify network registration started
OnNetworkEvent(NetworkEvent::Connecting);
// Wait for network ready with retry limit
int reg_retries = 0;
while (reg_retries < NETWORK_REG_MAX_RETRIES) {
auto result = modem_->WaitForNetworkReady();
if (result == NetworkStatus::Ready) {
break;
} else if (result == NetworkStatus::ErrorInsertPin) {
OnNetworkEvent(NetworkEvent::ModemErrorNoSim);
} else if (result == NetworkStatus::ErrorRegistrationDenied) {
OnNetworkEvent(NetworkEvent::ModemErrorRegDenied);
} else if (result == NetworkStatus::ErrorTimeout) {
OnNetworkEvent(NetworkEvent::ModemErrorTimeout);
}
reg_retries++;
vTaskDelay(pdMS_TO_TICKS(10000));
}
if (!modem_->network_ready()) {
ESP_LOGE(TAG, "Failed to register network after %d retries", NETWORK_REG_MAX_RETRIES);
return;
}
// 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());
}
void Ml307Board::StartNetwork() {
// Create network initialization task and return immediately
xTaskCreate([](void* arg) {
Ml307Board* board = static_cast<Ml307Board*>(arg);
board->NetworkTask();
vTaskDelete(NULL);
}, "ml307_net", 4096, this, 5, NULL);
}
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 <= 9) {
return FONT_AWESOME_SIGNAL_WEAK;
} else if (csq >= 10 && csq <= 14) {
return FONT_AWESOME_SIGNAL_FAIR;
} else if (csq >= 15 && csq <= 19) {
return FONT_AWESOME_SIGNAL_GOOD;
} else if (csq >= 20 && 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::SetPowerSaveLevel(PowerSaveLevel level) {
// TODO: Implement power save level for ML307
(void)level;
}
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
auto theme = display->GetTheme();
if (theme != nullptr) {
cJSON_AddStringToObject(screen, "theme", theme->name().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;
}

View File

@@ -0,0 +1,38 @@
#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_;
NetworkEventCallback network_event_callback_;
virtual std::string GetBoardJson() override;
// Internal helper to trigger network event callback
void OnNetworkEvent(NetworkEvent event, const std::string& data = "");
// Network initialization task (runs in FreeRTOS task)
static void NetworkTaskEntry(void* arg);
void NetworkTask();
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 void SetNetworkEventCallback(NetworkEventCallback callback) override;
virtual NetworkInterface* GetNetwork() override;
virtual const char* GetNetworkStateIcon() override;
virtual void SetPowerSaveLevel(PowerSaveLevel level) override;
virtual AudioCodec* GetAudioCodec() override { return nullptr; }
virtual std::string GetDeviceStatusJson() override;
};
#endif // ML307_BOARD_H

View File

@@ -0,0 +1,269 @@
#include "nt26_board.h"
#include "display.h"
#include "application.h"
#include "audio_codec.h"
#include <esp_log.h>
#include <font_awesome.h>
#include <cJSON.h>
#define TAG "Nt26Board"
Nt26Board::Nt26Board(gpio_num_t tx_pin, gpio_num_t rx_pin, gpio_num_t dtr_pin, gpio_num_t ri_pin, gpio_num_t reset_pin)
: tx_pin_(tx_pin), rx_pin_(rx_pin), dtr_pin_(dtr_pin), ri_pin_(ri_pin), reset_pin_(reset_pin) {
gpio_install_isr_service(ESP_INTR_FLAG_IRAM);
esp_event_loop_create_default();
esp_netif_init();
// Create PM lock handle
esp_pm_lock_create(ESP_PM_CPU_FREQ_MAX, 0, "nt26_cpu", &pm_lock_cpu_max_);
// Create network ready timeout timer
esp_timer_create_args_t timer_args = {
.callback = OnNetworkReadyTimeout,
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "nt26_net_timer",
.skip_unhandled_events = true
};
esp_timer_create(&timer_args, &network_ready_timer_);
}
Nt26Board::~Nt26Board() {
if (current_power_level_ != PowerSaveLevel::LOW_POWER) {
SetPowerSaveLevel(PowerSaveLevel::LOW_POWER);
}
if (network_ready_timer_) {
esp_timer_stop(network_ready_timer_);
esp_timer_delete(network_ready_timer_);
}
if (modem_) {
modem_->Stop();
}
if (pm_lock_cpu_max_) {
esp_pm_lock_delete(pm_lock_cpu_max_);
}
}
std::string Nt26Board::GetBoardType() {
return "nt26";
}
void Nt26Board::OnNetworkEvent(NetworkEvent event, const std::string& data) {
if (network_event_callback_) {
network_event_callback_(event, data);
}
}
void Nt26Board::OnNetworkReadyTimeout(void* arg) {
auto* self = static_cast<Nt26Board*>(arg);
ESP_LOGW(TAG, "Network ready timeout");
self->OnNetworkEvent(NetworkEvent::ModemErrorTimeout, "网络连接超时");
}
void Nt26Board::StartNetwork() {
OnNetworkEvent(NetworkEvent::ModemDetecting);
UartEthModem::Config config = {
.uart_num = UART_NUM_1,
.baud_rate = 3000000,
.tx_pin = tx_pin_,
.rx_pin = rx_pin_,
.mrdy_pin = dtr_pin_,
.srdy_pin = ri_pin_
};
modem_ = std::make_unique<UartEthModem>(config);
modem_->SetDebug(false);
modem_->SetNetworkEventCallback([this](UartEthModem::UartEthModemEvent event) {
switch (event) {
case UartEthModem::UartEthModemEvent::Connected:
esp_timer_stop(network_ready_timer_);
OnNetworkEvent(NetworkEvent::Connected);
break;
case UartEthModem::UartEthModemEvent::Disconnected:
OnNetworkEvent(NetworkEvent::Disconnected);
break;
case UartEthModem::UartEthModemEvent::ErrorNoSim:
esp_timer_stop(network_ready_timer_);
ScheduleAsyncStop();
OnNetworkEvent(NetworkEvent::ModemErrorNoSim);
break;
case UartEthModem::UartEthModemEvent::ErrorRegistrationDenied:
esp_timer_stop(network_ready_timer_);
ScheduleAsyncStop();
OnNetworkEvent(NetworkEvent::ModemErrorRegDenied);
break;
case UartEthModem::UartEthModemEvent::Connecting:
OnNetworkEvent(NetworkEvent::Connecting);
break;
case UartEthModem::UartEthModemEvent::ErrorInitFailed:
case UartEthModem::UartEthModemEvent::ErrorNoCarrier:
esp_timer_stop(network_ready_timer_);
ScheduleAsyncStop();
OnNetworkEvent(NetworkEvent::ModemErrorInitFailed);
break;
case UartEthModem::UartEthModemEvent::InFlightMode:
ESP_LOGW(TAG, "Modem in flight mode");
break;
}
});
if (modem_->Start() != ESP_OK) {
OnNetworkEvent(NetworkEvent::ModemErrorInitFailed);
return;
}
esp_timer_start_once(network_ready_timer_, 30000 * 1000ULL);
OnNetworkEvent(NetworkEvent::Connecting);
}
void Nt26Board::ScheduleAsyncStop() {
Application::GetInstance().Schedule([this]() {
if (modem_) {
modem_->Stop();
}
});
}
void Nt26Board::SetNetworkEventCallback(NetworkEventCallback callback) {
network_event_callback_ = std::move(callback);
}
NetworkInterface* Nt26Board::GetNetwork() {
static EspNetwork network;
return &network;
}
const char* Nt26Board::GetNetworkStateIcon() {
if (modem_ == nullptr || !modem_->IsInitialized()) {
return FONT_AWESOME_SIGNAL_OFF;
}
int csq = modem_->GetSignalStrength();
if (csq == 99 || csq == -1) {
return FONT_AWESOME_SIGNAL_OFF;
} else if (csq >= 0 && csq <= 9) {
return FONT_AWESOME_SIGNAL_WEAK;
} else if (csq >= 10 && csq <= 14) {
return FONT_AWESOME_SIGNAL_FAIR;
} else if (csq >= 15 && csq <= 19) {
return FONT_AWESOME_SIGNAL_GOOD;
} else if (csq >= 20 && csq <= 31) {
return FONT_AWESOME_SIGNAL_STRONG;
}
return FONT_AWESOME_SIGNAL_OFF;
}
void Nt26Board::SetPowerSaveLevel(PowerSaveLevel level) {
if (level == current_power_level_) return;
if (current_power_level_ == PowerSaveLevel::BALANCED ||
current_power_level_ == PowerSaveLevel::PERFORMANCE) {
if (pm_lock_cpu_max_) {
esp_pm_lock_release(pm_lock_cpu_max_);
}
}
if (level == PowerSaveLevel::BALANCED || level == PowerSaveLevel::PERFORMANCE) {
if (pm_lock_cpu_max_) {
esp_pm_lock_acquire(pm_lock_cpu_max_);
}
}
current_power_level_ = level;
}
std::string Nt26Board::GetBoardJson() {
// Set the board type for OTA
std::string board_json = std::string("{\"type\":\"" BOARD_TYPE "\",");
board_json += "\"name\":\"" BOARD_NAME "\",";
if (modem_) {
board_json += "\"revision\":\"" + modem_->GetModuleRevision() + "\",";
board_json += "\"carrier\":\"" + modem_->GetCarrierName() + "\",";
board_json += "\"csq\":\"" + std::to_string(modem_->GetSignalStrength()) + "\",";
board_json += "\"imei\":\"" + modem_->GetImei() + "\",";
board_json += "\"iccid\":\"" + modem_->GetIccid() + "\",";
board_json += "\"cereg\":" + GetRegistrationState().ToString() + "}";
} else {
board_json += "\"status\":\"offline\"}";
}
return board_json;
}
Nt26CeregState Nt26Board::GetRegistrationState() {
Nt26CeregState state;
if (modem_) {
auto cell_info = modem_->GetCellInfo();
state.stat = cell_info.stat;
state.tac = cell_info.tac;
state.ci = cell_info.ci;
state.AcT = cell_info.act;
}
return state;
}
std::string Nt26Board::GetDeviceStatusJson() {
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
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) {
auto theme = display->GetTheme();
if (theme != nullptr) {
cJSON_AddStringToObject(screen, "theme", theme->name().c_str());
}
}
cJSON_AddItemToObject(root, "screen", screen);
// Battery
int battery_level = 0;
bool charging = false, discharging = false;
if (board.GetBatteryLevel(battery_level, charging, discharging)) {
auto 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");
if (modem_) {
cJSON_AddStringToObject(network, "carrier", modem_->GetCarrierName().c_str());
int csq = modem_->GetSignalStrength();
if (csq == 99 || csq == -1) {
cJSON_AddStringToObject(network, "signal", "unknown");
} else if (csq >= 0 && csq <= 14) {
cJSON_AddStringToObject(network, "signal", "weak");
} else if (csq >= 15 && 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;
}

View File

@@ -0,0 +1,62 @@
#ifndef NT26_BOARD_H
#define NT26_BOARD_H
#include <memory>
#include <uart_eth_modem.h>
#include <esp_network.h>
#include <esp_pm.h>
#include <esp_timer.h>
#include "board.h"
struct Nt26CeregState {
int stat = 0;
std::string tac;
std::string ci;
int AcT = -1;
std::string ToString() const {
std::string json = "{";
json += "\"stat\":" + std::to_string(stat);
if (!tac.empty()) json += ",\"tac\":\"" + tac + "\"";
if (!ci.empty()) json += ",\"ci\":\"" + ci + "\"";
if (AcT >= 0) json += ",\"AcT\":" + std::to_string(AcT);
json += "}";
return json;
}
};
class Nt26Board : public Board {
protected:
std::unique_ptr<UartEthModem> modem_;
gpio_num_t tx_pin_;
gpio_num_t rx_pin_;
gpio_num_t dtr_pin_; // mrdy_pin
gpio_num_t ri_pin_; // srdy_pin
gpio_num_t reset_pin_;
NetworkEventCallback network_event_callback_;
esp_pm_lock_handle_t pm_lock_cpu_max_ = nullptr;
PowerSaveLevel current_power_level_ = PowerSaveLevel::LOW_POWER;
esp_timer_handle_t network_ready_timer_ = nullptr;
virtual std::string GetBoardJson() override;
void OnNetworkEvent(NetworkEvent event, const std::string& data = "");
static void OnNetworkReadyTimeout(void* arg);
void ScheduleAsyncStop();
public:
Nt26Board(gpio_num_t tx_pin, gpio_num_t rx_pin, gpio_num_t dtr_pin, gpio_num_t ri_pin, gpio_num_t reset_pin = GPIO_NUM_NC);
virtual ~Nt26Board();
virtual std::string GetBoardType() override;
virtual void StartNetwork() override;
virtual void SetNetworkEventCallback(NetworkEventCallback callback) override;
virtual NetworkInterface* GetNetwork() override;
virtual const char* GetNetworkStateIcon() override;
virtual void SetPowerSaveLevel(PowerSaveLevel level) override;
virtual AudioCodec* GetAudioCodec() override { return nullptr; }
virtual std::string GetDeviceStatusJson() override;
Nt26CeregState GetRegistrationState();
};
#endif // NT26_BOARD_H

View 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_();
}
}
}

View 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_;
};

View 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);
}

View 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

View File

@@ -0,0 +1,247 @@
#include "rndis_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 <utility>
#include <font_awesome.h>
#if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3
static const char *TAG = "RndisBoard";
#define EVENT_GOT_IP_BIT (1 << 0)
RndisBoard::RndisBoard() {
}
RndisBoard::~RndisBoard() {
}
std::string RndisBoard::GetBoardType() {
return "rndis";
}
void RndisBoard::StartNetwork() {
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
/* NVS partition was truncated and needs to be erased
* Retry nvs_flash_init */
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
/* Initialize default TCP/IP stack */
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
s_event_group = xEventGroupCreate();
esp_event_handler_register(IOT_ETH_EVENT, ESP_EVENT_ANY_ID, iot_event_handle, this);
esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, iot_event_handle, this);
// install usbh cdc driver
usbh_cdc_driver_config_t config = {
.task_stack_size = 1024 * 4,
.task_priority = configMAX_PRIORITIES - 1,
.task_coreid = 0,
.skip_init_usb_host_driver = false,
};
ESP_ERROR_CHECK(usbh_cdc_driver_install(&config));
install_rndis(USB_DEVICE_VENDOR_ANY, USB_DEVICE_PRODUCT_ANY, "USB RNDIS0");
xEventGroupWaitBits(s_event_group, EVENT_GOT_IP_BIT, pdFALSE, pdFALSE, portMAX_DELAY);
}
void RndisBoard::iot_event_handle(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
if (event_base == IOT_ETH_EVENT) {
switch (event_id) {
case IOT_ETH_EVENT_START:
ESP_LOGI(TAG, "IOT_ETH_EVENT_START");
break;
case IOT_ETH_EVENT_STOP:
ESP_LOGI(TAG, "IOT_ETH_EVENT_STOP");
break;
case IOT_ETH_EVENT_CONNECTED:
ESP_LOGI(TAG, "IOT_ETH_EVENT_CONNECTED");
static_cast<RndisBoard*>(arg)->OnNetworkEvent(NetworkEvent::Connected);
break;
case IOT_ETH_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "IOT_ETH_EVENT_DISCONNECTED");
xEventGroupClearBits(static_cast<RndisBoard*>(arg)->s_event_group, EVENT_GOT_IP_BIT);
static_cast<RndisBoard*>(arg)->OnNetworkEvent(NetworkEvent::Disconnected);
break;
default:
ESP_LOGI(TAG, "IOT_ETH_EVENT_UNKNOWN");
break;
}
} else if (event_base == IP_EVENT) {
ESP_LOGI(TAG, "GOT_IP");
xEventGroupSetBits(static_cast<RndisBoard*>(arg)->s_event_group, EVENT_GOT_IP_BIT);
}
}
void RndisBoard::OnNetworkEvent(NetworkEvent event, const std::string& data) {
switch (event) {
case NetworkEvent::Connected:
ESP_LOGI(TAG, "Connected to WiFi: %s", data.c_str());
break;
case NetworkEvent::Scanning:
ESP_LOGI(TAG, "WiFi scanning");
break;
case NetworkEvent::Connecting:
ESP_LOGI(TAG, "WiFi connecting to %s", data.c_str());
break;
case NetworkEvent::Disconnected:
ESP_LOGW(TAG, "WiFi disconnected");
break;
default:
break;
}
// Notify external callback if set
if (network_event_callback_) {
network_event_callback_(event, data);
}
}
void RndisBoard::SetNetworkEventCallback(NetworkEventCallback callback) {
network_event_callback_ = std::move(callback);
}
void RndisBoard::install_rndis(uint16_t idVendor, uint16_t idProduct, const char *netif_name)
{
esp_err_t ret = ESP_OK;
iot_eth_handle_t eth_handle = nullptr;
iot_eth_netif_glue_handle_t glue = nullptr;
usb_device_match_id_t *dev_match_id = (usb_device_match_id_t*)calloc(2, sizeof(usb_device_match_id_t));
dev_match_id[0].match_flags = USB_DEVICE_ID_MATCH_VID_PID;
dev_match_id[0].idVendor = idVendor;
dev_match_id[0].idProduct = idProduct;
memset(&dev_match_id[1], 0, sizeof(usb_device_match_id_t)); // end of list
iot_usbh_rndis_config_t rndis_cfg = {
.match_id_list = dev_match_id,
};
ret = iot_eth_new_usb_rndis(&rndis_cfg, &rndis_eth_driver);
if (ret != ESP_OK || rndis_eth_driver == NULL) {
ESP_LOGE(TAG, "Failed to create USB RNDIS driver");
return;
}
iot_eth_config_t eth_cfg = {
.driver = rndis_eth_driver,
.stack_input = NULL,
};
ret = iot_eth_install(&eth_cfg, &eth_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to install USB RNDIS driver");
return;
}
esp_netif_inherent_config_t _inherent_eth_config = ESP_NETIF_INHERENT_DEFAULT_ETH();
_inherent_eth_config.if_key = netif_name;
_inherent_eth_config.if_desc = netif_name;
esp_netif_config_t netif_cfg = {
.base = &_inherent_eth_config,
.driver = NULL,
.stack = ESP_NETIF_NETSTACK_DEFAULT_ETH,
};
s_rndis_netif = esp_netif_new(&netif_cfg);
if (s_rndis_netif == NULL) {
ESP_LOGE(TAG, "Failed to create network interface");
return;
}
glue = iot_eth_new_netif_glue(eth_handle);
if (glue == NULL) {
ESP_LOGE(TAG, "Failed to create netif glue");
return;
}
esp_netif_attach(s_rndis_netif, glue);
iot_eth_start(eth_handle);
}
NetworkInterface* RndisBoard::GetNetwork() {
static EspNetwork network;
return &network;
}
const char* RndisBoard::GetNetworkStateIcon() {
return FONT_AWESOME_SIGNAL_STRONG;
}
std::string RndisBoard::GetBoardJson() {
std::string json = R"({"type":")" + std::string(BOARD_TYPE) + R"(",)";
json += R"("name":")" + std::string(BOARD_NAME) + R"(",)";
json += R"("mac":")" + SystemInfo::GetMacAddress() + R"("})";
return json;
}
void RndisBoard::SetPowerSaveLevel(PowerSaveLevel level) {
}
std::string RndisBoard::GetDeviceStatusJson() {
auto& board = Board::GetInstance();
auto root = cJSON_CreateObject();
// Audio speaker
auto audio_speaker = cJSON_CreateObject();
if (auto codec = board.GetAudioCodec()) {
cJSON_AddNumberToObject(audio_speaker, "volume", codec->output_volume());
}
cJSON_AddItemToObject(root, "audio_speaker", audio_speaker);
// Screen
auto screen = cJSON_CreateObject();
if (auto backlight = board.GetBacklight()) {
cJSON_AddNumberToObject(screen, "brightness", backlight->brightness());
}
if (auto display = board.GetDisplay(); display && display->height() > 64) {
if (auto theme = display->GetTheme()) {
cJSON_AddStringToObject(screen, "theme", theme->name().c_str());
}
}
cJSON_AddItemToObject(root, "screen", screen);
// Battery
int level = 0;
bool charging = false, discharging = false;
if (board.GetBatteryLevel(level, charging, discharging)) {
auto battery = cJSON_CreateObject();
cJSON_AddNumberToObject(battery, "level", level);
cJSON_AddBoolToObject(battery, "charging", charging);
cJSON_AddItemToObject(root, "battery", battery);
}
// Network
auto network = cJSON_CreateObject();
cJSON_AddStringToObject(network, "type", "rndis");
cJSON_AddItemToObject(root, "network", network);
// Chip temperature
float temp = 0.0f;
if (board.GetTemperature(temp)) {
auto chip = cJSON_CreateObject();
cJSON_AddNumberToObject(chip, "temperature", temp);
cJSON_AddItemToObject(root, "chip", chip);
}
auto str = cJSON_PrintUnformatted(root);
std::string result(str);
cJSON_free(str);
cJSON_Delete(root);
return result;
}
#endif // CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3

View File

@@ -0,0 +1,74 @@
#ifndef RNDIS_BOARD_H
#define RNDIS_BOARD_H
#include "sdkconfig.h"
#if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3
#include "board.h"
#include "iot_eth.h"
#include "iot_usbh_rndis.h"
#include "iot_eth_netif_glue.h"
#include <esp_netif.h>
#include <esp_event.h>
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <esp_timer.h>
class RndisBoard : public Board {
private:
EventGroupHandle_t s_event_group = nullptr;
iot_eth_driver_t *rndis_eth_driver = nullptr;
esp_netif_t *s_rndis_netif = nullptr;
void install_rndis(uint16_t idVendor, uint16_t idProduct, const char *netif_name);
static void iot_event_handle(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
protected:
NetworkEventCallback network_event_callback_ = nullptr;
virtual std::string GetBoardJson() override;
/**
* Handle network event (called from WiFi manager callbacks)
* @param event The network event type
* @param data Additional data (e.g., SSID for Connecting/Connected events)
*/
void OnNetworkEvent(NetworkEvent event, const std::string& data = "");
/**
* Start WiFi connection attempt
*/
void TryWifiConnect();
/**
* Enter WiFi configuration mode
*/
void StartWifiConfigMode();
/**
* WiFi connection timeout callback
*/
static void OnWifiConnectTimeout(void* arg);
public:
RndisBoard();
virtual ~RndisBoard();
virtual std::string GetBoardType() override;
/**
* Start network connection asynchronously
* This function returns immediately. Network events are notified through the callback set by SetNetworkEventCallback().
*/
virtual void StartNetwork() override;
virtual NetworkInterface* GetNetwork() override;
virtual void SetNetworkEventCallback(NetworkEventCallback callback) override;
virtual const char* GetNetworkStateIcon() override;
virtual void SetPowerSaveLevel(PowerSaveLevel level) override;
virtual AudioCodec* GetAudioCodec() override { return nullptr; }
virtual std::string GetDeviceStatusJson() override;
};
#endif // CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3
#endif // RNDIS_BOARD_H

View 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_();
}
}
}

View 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_;
};

View 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);
}

View 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

View 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();
}

View 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

View File

@@ -0,0 +1,357 @@
#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 <utility>
#include <font_awesome.h>
#include <wifi_manager.h>
#include <wifi_station.h>
#include <ssid_manager.h>
#include "afsk_demod.h"
#ifdef CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING
#include "blufi.h"
#endif
static const char *TAG = "WifiBoard";
// Connection timeout in seconds
static constexpr int CONNECT_TIMEOUT_SEC = 60;
WifiBoard::WifiBoard() {
// Create connection timeout timer
esp_timer_create_args_t timer_args = {
.callback = OnWifiConnectTimeout,
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "wifi_connect_timer",
.skip_unhandled_events = true
};
esp_timer_create(&timer_args, &connect_timer_);
}
WifiBoard::~WifiBoard() {
if (connect_timer_) {
esp_timer_stop(connect_timer_);
esp_timer_delete(connect_timer_);
}
}
std::string WifiBoard::GetBoardType() {
return "wifi";
}
void WifiBoard::StartNetwork() {
auto& wifi_manager = WifiManager::GetInstance();
// Initialize WiFi manager
WifiManagerConfig config;
config.ssid_prefix = "Xiaozhi";
config.language = Lang::CODE;
wifi_manager.Initialize(config);
// Set unified event callback - forward to NetworkEvent with SSID data
wifi_manager.SetEventCallback([this](WifiEvent event, const std::string& data) {
switch (event) {
case WifiEvent::Scanning:
OnNetworkEvent(NetworkEvent::Scanning);
break;
case WifiEvent::Connecting:
OnNetworkEvent(NetworkEvent::Connecting, data);
break;
case WifiEvent::Connected:
OnNetworkEvent(NetworkEvent::Connected, data);
break;
case WifiEvent::Disconnected:
OnNetworkEvent(NetworkEvent::Disconnected);
break;
case WifiEvent::ConfigModeEnter:
OnNetworkEvent(NetworkEvent::WifiConfigModeEnter);
break;
case WifiEvent::ConfigModeExit:
OnNetworkEvent(NetworkEvent::WifiConfigModeExit);
break;
}
});
// Try to connect or enter config mode
TryWifiConnect();
}
void WifiBoard::TryWifiConnect() {
auto& ssid_manager = SsidManager::GetInstance();
bool have_ssid = !ssid_manager.GetSsidList().empty();
if (have_ssid) {
// Start connection attempt with timeout
ESP_LOGI(TAG, "Starting WiFi connection attempt");
esp_timer_start_once(connect_timer_, CONNECT_TIMEOUT_SEC * 1000000ULL);
WifiManager::GetInstance().StartStation();
} else {
// No SSID configured, enter config mode
// Wait for the board version to be shown
vTaskDelay(pdMS_TO_TICKS(1500));
StartWifiConfigMode();
}
}
void WifiBoard::OnNetworkEvent(NetworkEvent event, const std::string& data) {
switch (event) {
case NetworkEvent::Connected:
// Stop timeout timer
esp_timer_stop(connect_timer_);
#ifdef CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING
// make sure blufi resources has been released
Blufi::GetInstance().deinit();
#endif
in_config_mode_ = false;
ESP_LOGI(TAG, "Connected to WiFi: %s", data.c_str());
break;
case NetworkEvent::Scanning:
ESP_LOGI(TAG, "WiFi scanning");
break;
case NetworkEvent::Connecting:
ESP_LOGI(TAG, "WiFi connecting to %s", data.c_str());
break;
case NetworkEvent::Disconnected:
ESP_LOGW(TAG, "WiFi disconnected");
break;
case NetworkEvent::WifiConfigModeEnter:
ESP_LOGI(TAG, "WiFi config mode entered");
in_config_mode_ = true;
break;
case NetworkEvent::WifiConfigModeExit:
ESP_LOGI(TAG, "WiFi config mode exited");
in_config_mode_ = false;
// Try to connect with the new credentials
TryWifiConnect();
break;
default:
break;
}
// Notify external callback if set
if (network_event_callback_) {
network_event_callback_(event, data);
}
}
void WifiBoard::SetNetworkEventCallback(NetworkEventCallback callback) {
network_event_callback_ = std::move(callback);
}
void WifiBoard::OnWifiConnectTimeout(void* arg) {
auto* board = static_cast<WifiBoard*>(arg);
ESP_LOGW(TAG, "WiFi connection timeout, entering config mode");
WifiManager::GetInstance().StopStation();
board->StartWifiConfigMode();
}
void WifiBoard::StartWifiConfigMode() {
in_config_mode_ = true;
// Transition to wifi configuring state
Application::GetInstance().SetDeviceState(kDeviceStateWifiConfiguring);
#ifdef CONFIG_USE_HOTSPOT_WIFI_PROVISIONING
auto& wifi_manager = WifiManager::GetInstance();
wifi_manager.StartConfigAp();
// Show config prompt after a short delay
Application::GetInstance().Schedule([&wifi_manager]() {
std::string hint = Lang::Strings::CONNECT_TO_HOTSPOT;
hint += wifi_manager.GetApSsid();
hint += Lang::Strings::ACCESS_VIA_BROWSER;
hint += wifi_manager.GetApWebUrl();
Application::GetInstance().Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "gear", Lang::Sounds::OGG_WIFICONFIG);
});
#elif CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING
auto &blufi = Blufi::GetInstance();
// initialize esp-blufi protocol
blufi.init();
#endif
#if CONFIG_USE_ACOUSTIC_WIFI_PROVISIONING
// Start acoustic provisioning task
auto codec = Board::GetInstance().GetAudioCodec();
int channel = codec ? codec->input_channels() : 1;
ESP_LOGI(TAG, "Starting acoustic WiFi provisioning, channels: %d", channel);
xTaskCreate([](void* arg) {
auto ch = reinterpret_cast<intptr_t>(arg);
auto& app = Application::GetInstance();
auto& wifi = WifiManager::GetInstance();
auto disp = Board::GetInstance().GetDisplay();
audio_wifi_config::ReceiveWifiCredentialsFromAudio(&app, &wifi, disp, ch);
vTaskDelete(NULL);
}, "acoustic_wifi", 4096, reinterpret_cast<void*>(channel), 2, NULL);
#endif
}
void WifiBoard::EnterWifiConfigMode() {
ESP_LOGI(TAG, "EnterWifiConfigMode called");
GetDisplay()->ShowNotification(Lang::Strings::ENTERING_WIFI_CONFIG_MODE);
auto& app = Application::GetInstance();
auto state = app.GetDeviceState();
if (state == kDeviceStateSpeaking || state == kDeviceStateListening || state == kDeviceStateIdle) {
// Reset protocol (close audio channel, reset protocol)
Application::GetInstance().ResetProtocol();
xTaskCreate([](void* arg) {
auto* board = static_cast<WifiBoard*>(arg);
// Wait for 1 second to allow speaking to finish gracefully
vTaskDelay(pdMS_TO_TICKS(1000));
// Stop any ongoing connection attempt
esp_timer_stop(board->connect_timer_);
WifiManager::GetInstance().StopStation();
// Enter config mode
board->StartWifiConfigMode();
vTaskDelete(NULL);
}, "wifi_cfg_delay", 4096, this, 2, NULL);
return;
}
if (state != kDeviceStateStarting) {
ESP_LOGE(TAG, "EnterWifiConfigMode called but device state is not starting or speaking, device state: %d", state);
return;
}
// Stop any ongoing connection attempt
esp_timer_stop(connect_timer_);
WifiManager::GetInstance().StopStation();
StartWifiConfigMode();
}
bool WifiBoard::IsInWifiConfigMode() const {
return WifiManager::GetInstance().IsConfigMode();
}
NetworkInterface* WifiBoard::GetNetwork() {
static EspNetwork network;
return &network;
}
const char* WifiBoard::GetNetworkStateIcon() {
auto& wifi = WifiManager::GetInstance();
if (wifi.IsConfigMode()) {
return FONT_AWESOME_WIFI;
}
if (!wifi.IsConnected()) {
return FONT_AWESOME_WIFI_SLASH;
}
int rssi = wifi.GetRssi();
if (rssi >= -65) {
return FONT_AWESOME_WIFI;
} else if (rssi >= -75) {
return FONT_AWESOME_WIFI_FAIR;
}
return FONT_AWESOME_WIFI_WEAK;
}
std::string WifiBoard::GetBoardJson() {
auto& wifi = WifiManager::GetInstance();
std::string json = R"({"type":")" + std::string(BOARD_TYPE) + R"(",)";
json += R"("name":")" + std::string(BOARD_NAME) + R"(",)";
if (!wifi.IsConfigMode()) {
json += R"("ssid":")" + wifi.GetSsid() + R"(",)";
json += R"("rssi":)" + std::to_string(wifi.GetRssi()) + R"(,)";
json += R"("channel":)" + std::to_string(wifi.GetChannel()) + R"(,)";
json += R"("ip":")" + wifi.GetIpAddress() + R"(",)";
}
json += R"("mac":")" + SystemInfo::GetMacAddress() + R"("})";
return json;
}
void WifiBoard::SetPowerSaveLevel(PowerSaveLevel level) {
WifiPowerSaveLevel wifi_level;
switch (level) {
case PowerSaveLevel::LOW_POWER:
wifi_level = WifiPowerSaveLevel::LOW_POWER;
break;
case PowerSaveLevel::BALANCED:
wifi_level = WifiPowerSaveLevel::BALANCED;
break;
case PowerSaveLevel::PERFORMANCE:
default:
wifi_level = WifiPowerSaveLevel::PERFORMANCE;
break;
}
WifiManager::GetInstance().SetPowerSaveLevel(wifi_level);
}
std::string WifiBoard::GetDeviceStatusJson() {
auto& board = Board::GetInstance();
auto root = cJSON_CreateObject();
// Audio speaker
auto audio_speaker = cJSON_CreateObject();
if (auto codec = board.GetAudioCodec()) {
cJSON_AddNumberToObject(audio_speaker, "volume", codec->output_volume());
}
cJSON_AddItemToObject(root, "audio_speaker", audio_speaker);
// Screen
auto screen = cJSON_CreateObject();
if (auto backlight = board.GetBacklight()) {
cJSON_AddNumberToObject(screen, "brightness", backlight->brightness());
}
if (auto display = board.GetDisplay(); display && display->height() > 64) {
if (auto theme = display->GetTheme()) {
cJSON_AddStringToObject(screen, "theme", theme->name().c_str());
}
}
cJSON_AddItemToObject(root, "screen", screen);
// Battery
int level = 0;
bool charging = false, discharging = false;
if (board.GetBatteryLevel(level, charging, discharging)) {
auto battery = cJSON_CreateObject();
cJSON_AddNumberToObject(battery, "level", level);
cJSON_AddBoolToObject(battery, "charging", charging);
cJSON_AddItemToObject(root, "battery", battery);
}
// Network
auto& wifi = WifiManager::GetInstance();
auto network = cJSON_CreateObject();
cJSON_AddStringToObject(network, "type", "wifi");
cJSON_AddStringToObject(network, "ssid", wifi.GetSsid().c_str());
int rssi = wifi.GetRssi();
const char* signal = rssi >= -60 ? "strong" : (rssi >= -70 ? "medium" : "weak");
cJSON_AddStringToObject(network, "signal", signal);
cJSON_AddItemToObject(root, "network", network);
// Chip temperature
float temp = 0.0f;
if (board.GetTemperature(temp)) {
auto chip = cJSON_CreateObject();
cJSON_AddNumberToObject(chip, "temperature", temp);
cJSON_AddItemToObject(root, "chip", chip);
}
auto str = cJSON_PrintUnformatted(root);
std::string result(str);
cJSON_free(str);
cJSON_Delete(root);
return result;
}

View File

@@ -0,0 +1,69 @@
#ifndef WIFI_BOARD_H
#define WIFI_BOARD_H
#include "board.h"
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <esp_timer.h>
class WifiBoard : public Board {
protected:
esp_timer_handle_t connect_timer_ = nullptr;
bool in_config_mode_ = false;
NetworkEventCallback network_event_callback_ = nullptr;
virtual std::string GetBoardJson() override;
/**
* Handle network event (called from WiFi manager callbacks)
* @param event The network event type
* @param data Additional data (e.g., SSID for Connecting/Connected events)
*/
void OnNetworkEvent(NetworkEvent event, const std::string& data = "");
/**
* Start WiFi connection attempt
*/
void TryWifiConnect();
/**
* Enter WiFi configuration mode
*/
void StartWifiConfigMode();
/**
* WiFi connection timeout callback
*/
static void OnWifiConnectTimeout(void* arg);
public:
WifiBoard();
virtual ~WifiBoard();
virtual std::string GetBoardType() override;
/**
* Start network connection asynchronously
* This function returns immediately. Network events are notified through the callback set by SetNetworkEventCallback().
*/
virtual void StartNetwork() override;
virtual NetworkInterface* GetNetwork() override;
virtual void SetNetworkEventCallback(NetworkEventCallback callback) override;
virtual const char* GetNetworkStateIcon() override;
virtual void SetPowerSaveLevel(PowerSaveLevel level) override;
virtual AudioCodec* GetAudioCodec() override { return nullptr; }
virtual std::string GetDeviceStatusJson() override;
/**
* Enter WiFi configuration mode (thread-safe, can be called from any task)
*/
void EnterWifiConfigMode();
/**
* Check if in WiFi config mode
*/
bool IsInWifiConfigMode() const;
};
#endif // WIFI_BOARD_H