first commit

This commit is contained in:
David R.
2025-09-05 18:58:05 +02:00
parent fc13e0779b
commit ed05c83ac6
3678 changed files with 832193 additions and 0 deletions
@@ -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);
}
@@ -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
@@ -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) : 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 = 25000, //背光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);
}
@@ -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);
~PwmBacklight();
void SetBrightnessImpl(uint8_t brightness) override;
};
@@ -0,0 +1,167 @@
#include "board.h"
#include "system_info.h"
#include "settings.h"
#include "display/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::GetJson() {
/*
{
"version": 2,
"flash_size": 4194304,
"psram_size": 0,
"minimum_free_heap_size": 123456,
"mac_address": "00:00:00:00:00:00",
"uuid": "00000000-0000-0000-0000-000000000000",
"chip_model_name": "esp32s3",
"chip_info": {
"model": 1,
"cores": 2,
"revision": 0,
"features": 0
},
"application": {
"name": "my-app",
"version": "1.0.0",
"compile_time": "2021-01-01T00:00:00Z"
"idf_version": "4.2-dev"
"elf_sha256": ""
},
"partition_table": [
"app": {
"label": "app",
"type": 1,
"subtype": 2,
"address": 0x10000,
"size": 0x100000
}
],
"ota": {
"label": "ota_0"
},
"board": {
...
}
}
*/
std::string json = "{";
json += "\"version\":2,";
json += "\"language\":\"" + std::string(Lang::CODE) + "\",";
json += "\"flash_size\":" + std::to_string(SystemInfo::GetFlashSize()) + ",";
json += "\"minimum_free_heap_size\":" + std::to_string(SystemInfo::GetMinimumFreeHeapSize()) + ",";
json += "\"mac_address\":\"" + SystemInfo::GetMacAddress() + "\",";
json += "\"uuid\":\"" + uuid_ + "\",";
json += "\"chip_model_name\":\"" + SystemInfo::GetChipModelName() + "\",";
json += "\"chip_info\":{";
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
json += "\"model\":" + std::to_string(chip_info.model) + ",";
json += "\"cores\":" + std::to_string(chip_info.cores) + ",";
json += "\"revision\":" + std::to_string(chip_info.revision) + ",";
json += "\"features\":" + std::to_string(chip_info.features);
json += "},";
json += "\"application\":{";
auto app_desc = esp_app_get_description();
json += "\"name\":\"" + std::string(app_desc->project_name) + "\",";
json += "\"version\":\"" + std::string(app_desc->version) + "\",";
json += "\"compile_time\":\"" + std::string(app_desc->date) + "T" + std::string(app_desc->time) + "Z\",";
json += "\"idf_version\":\"" + std::string(app_desc->idf_ver) + "\",";
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 += "\"elf_sha256\":\"" + std::string(sha256_str) + "\"";
json += "},";
json += "\"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 += "{";
json += "\"label\":\"" + std::string(partition->label) + "\",";
json += "\"type\":" + std::to_string(partition->type) + ",";
json += "\"subtype\":" + std::to_string(partition->subtype) + ",";
json += "\"address\":" + std::to_string(partition->address) + ",";
json += "\"size\":" + std::to_string(partition->size);
json += "},";
it = esp_partition_next(it);
}
json.pop_back(); // Remove the last comma
json += "],";
json += "\"ota\":{";
auto ota_partition = esp_ota_get_running_partition();
json += "\"label\":\"" + std::string(ota_partition->label) + "\"";
json += "},";
json += "\"board\":" + GetBoardJson();
// Close the JSON object
json += "}";
return json;
}
@@ -0,0 +1,62 @@
#ifndef BOARD_H
#define BOARD_H
#include <http.h>
#include <web_socket.h>
#include <mqtt.h>
#include <udp.h>
#include <string>
#include "led/led.h"
#include "backlight.h"
#include "camera.h"
void* create_board();
class AudioCodec;
class Display;
class Board {
private:
Board(const Board&) = delete; // 禁用拷贝构造函数
Board& operator=(const Board&) = delete; // 禁用赋值操作
protected:
Board();
std::string GenerateUuid();
// 软件生成的设备唯一标识
std::string uuid_;
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 Http* CreateHttp() = 0;
virtual WebSocket* CreateWebSocket() = 0;
virtual Mqtt* CreateMqtt() = 0;
virtual Udp* CreateUdp() = 0;
virtual void StartNetwork() = 0;
virtual const char* GetNetworkStateIcon() = 0;
virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging);
virtual std::string GetJson();
virtual void SetPowerSaveMode(bool enabled) = 0;
virtual std::string GetBoardJson() = 0;
virtual std::string GetDeviceStatusJson() = 0;
};
#define DECLARE_BOARD(BOARD_CLASS_NAME) \
void* create_board() { \
return new BOARD_CLASS_NAME(); \
}
#endif // BOARD_H
@@ -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) : 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 = false,
.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);
}
@@ -0,0 +1,43 @@
#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);
~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
#endif // BUTTON_H_
@@ -0,0 +1,15 @@
#ifndef CAMERA_H
#define CAMERA_H
#include <string>
class Camera {
public:
virtual void SetExplainUrl(const std::string& url, const std::string& token) = 0;
virtual bool Capture() = 0;
virtual bool SetHMirror(bool enabled) = 0;
virtual bool SetVFlip(bool enabled) = 0;
virtual std::string Explain(const std::string& question) = 0;
};
#endif // CAMERA_H
@@ -0,0 +1,105 @@
#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, size_t ml307_rx_buffer_size, int32_t default_net_type)
: Board(),
ml307_tx_pin_(ml307_tx_pin),
ml307_rx_pin_(ml307_rx_pin),
ml307_rx_buffer_size_(ml307_rx_buffer_size) {
// 从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_rx_buffer_size_);
} 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();
}
Http* DualNetworkBoard::CreateHttp() {
return current_board_->CreateHttp();
}
WebSocket* DualNetworkBoard::CreateWebSocket() {
return current_board_->CreateWebSocket();
}
Mqtt* DualNetworkBoard::CreateMqtt() {
return current_board_->CreateMqtt();
}
Udp* DualNetworkBoard::CreateUdp() {
return current_board_->CreateUdp();
}
const char* DualNetworkBoard::GetNetworkStateIcon() {
return current_board_->GetNetworkStateIcon();
}
void DualNetworkBoard::SetPowerSaveMode(bool enabled) {
current_board_->SetPowerSaveMode(enabled);
}
std::string DualNetworkBoard::GetBoardJson() {
return current_board_->GetBoardJson();
}
std::string DualNetworkBoard::GetDeviceStatusJson() {
return current_board_->GetDeviceStatusJson();
}
@@ -0,0 +1,62 @@
#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_;
size_t ml307_rx_buffer_size_;
// 从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, size_t ml307_rx_buffer_size = 4096, 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 Http* CreateHttp() override;
virtual WebSocket* CreateWebSocket() override;
virtual Mqtt* CreateMqtt() override;
virtual Udp* CreateUdp() override;
virtual const char* GetNetworkStateIcon() override;
virtual void SetPowerSaveMode(bool enabled) override;
virtual std::string GetBoardJson() override;
virtual std::string GetDeviceStatusJson() override;
};
#endif // DUAL_NETWORK_BOARD_H
@@ -0,0 +1,301 @@
#include "esp32_camera.h"
#include "mcp_server.h"
#include "display.h"
#include "board.h"
#include "system_info.h"
#include <esp_log.h>
#include <esp_heap_caps.h>
#include <img_converters.h>
#include <cstring>
#define TAG "Esp32Camera"
Esp32Camera::Esp32Camera(const camera_config_t& config) {
// camera init
esp_err_t err = esp_camera_init(&config); // 配置上面定义的参数
if (err != ESP_OK) {
ESP_LOGE(TAG, "Camera init failed with error 0x%x", err);
return;
}
sensor_t *s = esp_camera_sensor_get(); // 获取摄像头型号
if (s->id.PID == GC0308_PID) {
s->set_hmirror(s, 0); // 这里控制摄像头镜像 写1镜像 写0不镜像
}
// 初始化预览图片的内存
memset(&preview_image_, 0, sizeof(preview_image_));
preview_image_.header.magic = LV_IMAGE_HEADER_MAGIC;
preview_image_.header.cf = LV_COLOR_FORMAT_RGB565;
preview_image_.header.flags = LV_IMAGE_FLAGS_ALLOCATED | LV_IMAGE_FLAGS_MODIFIABLE;
switch (config.frame_size) {
case FRAMESIZE_SVGA:
preview_image_.header.w = 800;
preview_image_.header.h = 600;
break;
case FRAMESIZE_VGA:
preview_image_.header.w = 640;
preview_image_.header.h = 480;
break;
case FRAMESIZE_QVGA:
preview_image_.header.w = 320;
preview_image_.header.h = 240;
break;
case FRAMESIZE_128X128:
preview_image_.header.w = 128;
preview_image_.header.h = 128;
break;
case FRAMESIZE_240X240:
preview_image_.header.w = 240;
preview_image_.header.h = 240;
break;
default:
ESP_LOGE(TAG, "Unsupported frame size: %d, image preview will not be shown", config.frame_size);
preview_image_.data_size = 0;
preview_image_.data = nullptr;
return;
}
preview_image_.header.stride = preview_image_.header.w * 2;
preview_image_.data_size = preview_image_.header.w * preview_image_.header.h * 2;
preview_image_.data = (uint8_t*)heap_caps_malloc(preview_image_.data_size, MALLOC_CAP_SPIRAM);
if (preview_image_.data == nullptr) {
ESP_LOGE(TAG, "Failed to allocate memory for preview image");
return;
}
}
Esp32Camera::~Esp32Camera() {
if (fb_) {
esp_camera_fb_return(fb_);
fb_ = nullptr;
}
if (preview_image_.data) {
heap_caps_free((void*)preview_image_.data);
preview_image_.data = nullptr;
}
esp_camera_deinit();
}
void Esp32Camera::SetExplainUrl(const std::string& url, const std::string& token) {
explain_url_ = url;
explain_token_ = token;
}
bool Esp32Camera::Capture() {
if (encoder_thread_.joinable()) {
encoder_thread_.join();
}
int frames_to_get = 2;
// Try to get a stable frame
for (int i = 0; i < frames_to_get; i++) {
if (fb_ != nullptr) {
esp_camera_fb_return(fb_);
}
fb_ = esp_camera_fb_get();
if (fb_ == nullptr) {
ESP_LOGE(TAG, "Camera capture failed");
return false;
}
}
// 如果预览图片 buffer 为空,则跳过预览
// 但仍返回 true,因为此时图像可以上传至服务器
if (preview_image_.data_size == 0) {
ESP_LOGW(TAG, "Skip preview because of unsupported frame size");
return true;
}
if (preview_image_.data == nullptr) {
ESP_LOGE(TAG, "Preview image data is not initialized");
return true;
}
// 显示预览图片
auto display = Board::GetInstance().GetDisplay();
if (display != nullptr) {
auto src = (uint16_t*)fb_->buf;
auto dst = (uint16_t*)preview_image_.data;
size_t pixel_count = fb_->len / 2;
for (size_t i = 0; i < pixel_count; i++) {
// 交换每个16位字内的字节
dst[i] = __builtin_bswap16(src[i]);
}
display->SetPreviewImage(&preview_image_);
}
return true;
}
bool Esp32Camera::SetHMirror(bool enabled) {
sensor_t *s = esp_camera_sensor_get();
if (s == nullptr) {
ESP_LOGE(TAG, "Failed to get camera sensor");
return false;
}
esp_err_t err = s->set_hmirror(s, enabled);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set horizontal mirror: %d", err);
return false;
}
ESP_LOGI(TAG, "Camera horizontal mirror set to: %s", enabled ? "enabled" : "disabled");
return true;
}
bool Esp32Camera::SetVFlip(bool enabled) {
sensor_t *s = esp_camera_sensor_get();
if (s == nullptr) {
ESP_LOGE(TAG, "Failed to get camera sensor");
return false;
}
esp_err_t err = s->set_vflip(s, enabled);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set vertical flip: %d", err);
return false;
}
ESP_LOGI(TAG, "Camera vertical flip set to: %s", enabled ? "enabled" : "disabled");
return true;
}
/**
* @brief 将摄像头捕获的图像发送到远程服务器进行AI分析和解释
*
* 该函数将当前摄像头缓冲区中的图像编码为JPEG格式,并通过HTTP POST请求
* 以multipart/form-data的形式发送到指定的解释服务器。服务器将根据提供的
* 问题对图像进行AI分析并返回结果。
*
* 实现特点:
* - 使用独立线程编码JPEG,与主线程分离
* - 采用分块传输编码(chunked transfer encoding)优化内存使用
* - 通过队列机制实现编码线程和发送线程的数据同步
* - 支持设备ID、客户端ID和认证令牌的HTTP头部配置
*
* @param question 要向AI提出的关于图像的问题,将作为表单字段发送
* @return std::string 服务器返回的JSON格式响应字符串
* 成功时包含AI分析结果,失败时包含错误信息
* 格式示例:{"success": true, "result": "分析结果"}
* {"success": false, "message": "错误信息"}
*
* @note 调用此函数前必须先调用SetExplainUrl()设置服务器URL
* @note 函数会等待之前的编码线程完成后再开始新的处理
* @warning 如果摄像头缓冲区为空或网络连接失败,将返回错误信息
*/
std::string Esp32Camera::Explain(const std::string& question) {
if (explain_url_.empty()) {
return "{\"success\": false, \"message\": \"Image explain URL or token is not set\"}";
}
// 创建局部的 JPEG 队列, 40 entries is about to store 512 * 40 = 20480 bytes of JPEG data
QueueHandle_t jpeg_queue = xQueueCreate(40, sizeof(JpegChunk));
if (jpeg_queue == nullptr) {
ESP_LOGE(TAG, "Failed to create JPEG queue");
return "{\"success\": false, \"message\": \"Failed to create JPEG queue\"}";
}
// We spawn a thread to encode the image to JPEG
encoder_thread_ = std::thread([this, jpeg_queue]() {
frame2jpg_cb(fb_, 80, [](void* arg, size_t index, const void* data, size_t len) -> unsigned int {
auto jpeg_queue = (QueueHandle_t)arg;
JpegChunk chunk = {
.data = (uint8_t*)heap_caps_aligned_alloc(16, len, MALLOC_CAP_SPIRAM),
.len = len
};
memcpy(chunk.data, data, len);
xQueueSend(jpeg_queue, &chunk, portMAX_DELAY);
return len;
}, jpeg_queue);
});
auto http = Board::GetInstance().CreateHttp();
// 构造multipart/form-data请求体
std::string boundary = "----ESP32_CAMERA_BOUNDARY";
// 配置HTTP客户端,使用分块传输编码
http->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
http->SetHeader("Client-Id", Board::GetInstance().GetUuid().c_str());
if (!explain_token_.empty()) {
http->SetHeader("Authorization", "Bearer " + explain_token_);
}
http->SetHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
http->SetHeader("Transfer-Encoding", "chunked");
if (!http->Open("POST", explain_url_)) {
ESP_LOGE(TAG, "Failed to connect to explain URL");
// Clear the queue
encoder_thread_.join();
JpegChunk chunk;
while (xQueueReceive(jpeg_queue, &chunk, portMAX_DELAY) == pdPASS) {
if (chunk.data != nullptr) {
heap_caps_free(chunk.data);
} else {
break;
}
}
vQueueDelete(jpeg_queue);
return "{\"success\": false, \"message\": \"Failed to connect to explain URL\"}";
}
{
// 第一块:question字段
std::string question_field;
question_field += "--" + boundary + "\r\n";
question_field += "Content-Disposition: form-data; name=\"question\"\r\n";
question_field += "\r\n";
question_field += question + "\r\n";
http->Write(question_field.c_str(), question_field.size());
}
{
// 第二块:文件字段头部
std::string file_header;
file_header += "--" + boundary + "\r\n";
file_header += "Content-Disposition: form-data; name=\"file\"; filename=\"camera.jpg\"\r\n";
file_header += "Content-Type: image/jpeg\r\n";
file_header += "\r\n";
http->Write(file_header.c_str(), file_header.size());
}
// 第三块:JPEG数据
size_t total_sent = 0;
while (true) {
JpegChunk chunk;
if (xQueueReceive(jpeg_queue, &chunk, portMAX_DELAY) != pdPASS) {
ESP_LOGE(TAG, "Failed to receive JPEG chunk");
break;
}
if (chunk.data == nullptr) {
break; // The last chunk
}
http->Write((const char*)chunk.data, chunk.len);
total_sent += chunk.len;
heap_caps_free(chunk.data);
}
// Wait for the encoder thread to finish
encoder_thread_.join();
// 清理队列
vQueueDelete(jpeg_queue);
{
// 第四块:multipart尾部
std::string multipart_footer;
multipart_footer += "\r\n--" + boundary + "--\r\n";
http->Write(multipart_footer.c_str(), multipart_footer.size());
}
// 结束块
http->Write("", 0);
if (http->GetStatusCode() != 200) {
ESP_LOGE(TAG, "Failed to upload photo, status code: %d", http->GetStatusCode());
return "{\"success\": false, \"message\": \"Failed to upload photo\"}";
}
std::string result = http->ReadAll();
http->Close();
// Get remain task stack size
size_t remain_stack_size = uxTaskGetStackHighWaterMark(nullptr);
ESP_LOGI(TAG, "Explain image size=%dx%d, compressed size=%d, remain stack size=%d, question=%s\n%s",
fb_->width, fb_->height, total_sent, remain_stack_size, question.c_str(), result.c_str());
return result;
}
@@ -0,0 +1,39 @@
#ifndef ESP32_CAMERA_H
#define ESP32_CAMERA_H
#include <esp_camera.h>
#include <lvgl.h>
#include <thread>
#include <memory>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include "camera.h"
struct JpegChunk {
uint8_t* data;
size_t len;
};
class Esp32Camera : public Camera {
private:
camera_fb_t* fb_ = nullptr;
lv_img_dsc_t preview_image_;
std::string explain_url_;
std::string explain_token_;
std::thread encoder_thread_;
public:
Esp32Camera(const camera_config_t& config);
~Esp32Camera();
virtual void SetExplainUrl(const std::string& url, const std::string& token);
virtual bool Capture();
// 翻转控制函数
virtual bool SetHMirror(bool enabled) override;
virtual bool SetVFlip(bool enabled) override;
virtual std::string Explain(const std::string& question);
};
#endif // ESP32_CAMERA_H
@@ -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));
}
@@ -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
@@ -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);
}
}
@@ -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_
@@ -0,0 +1,44 @@
#ifndef __LAMP_CONTROLLER_H__
#define __LAMP_CONTROLLER_H__
#include "mcp_server.h"
class LampController {
private:
bool power_ = false;
gpio_num_t gpio_num_;
public:
LampController(gpio_num_t gpio_num) : gpio_num_(gpio_num) {
gpio_config_t config = {
.pin_bit_mask = (1ULL << gpio_num_),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&config));
gpio_set_level(gpio_num_, 0);
auto& mcp_server = McpServer::GetInstance();
mcp_server.AddTool("self.lamp.get_state", "Get the power state of the lamp", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
return power_ ? "{\"power\": true}" : "{\"power\": false}";
});
mcp_server.AddTool("self.lamp.turn_on", "Turn on the lamp", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
power_ = true;
gpio_set_level(gpio_num_, 1);
return true;
});
mcp_server.AddTool("self.lamp.turn_off", "Turn off the lamp", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
power_ = false;
gpio_set_level(gpio_num_, 0);
return true;
});
}
};
#endif // __LAMP_CONTROLLER_H__
@@ -0,0 +1,212 @@
#include "ml307_board.h"
#include "application.h"
#include "display.h"
#include "font_awesome_symbols.h"
#include "assets/lang_config.h"
#include <esp_log.h>
#include <esp_timer.h>
#include <ml307_http.h>
#include <ml307_ssl_transport.h>
#include <web_socket.h>
#include <ml307_mqtt.h>
#include <ml307_udp.h>
#include <opus_encoder.h>
static const char *TAG = "Ml307Board";
Ml307Board::Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, size_t rx_buffer_size) : modem_(tx_pin, rx_pin, rx_buffer_size) {
}
std::string Ml307Board::GetBoardType() {
return "ml307";
}
void Ml307Board::StartNetwork() {
auto display = Board::GetInstance().GetDisplay();
display->SetStatus(Lang::Strings::DETECTING_MODULE);
modem_.SetDebug(false);
modem_.SetBaudRate(921600);
auto& application = Application::GetInstance();
// If low power, the material ready event will be triggered by the modem because of a reset
modem_.OnMaterialReady([this, &application]() {
ESP_LOGI(TAG, "ML307 material ready");
application.Schedule([this, &application]() {
application.SetDeviceState(kDeviceStateIdle);
WaitForNetworkReady();
});
});
WaitForNetworkReady();
}
void Ml307Board::WaitForNetworkReady() {
auto& application = Application::GetInstance();
auto display = Board::GetInstance().GetDisplay();
display->SetStatus(Lang::Strings::REGISTERING_NETWORK);
while (true) {
int result = modem_.WaitForNetworkReady();
if (result == -1) {
application.Alert(Lang::Strings::ERROR, Lang::Strings::PIN_ERROR, "sad", Lang::Sounds::P3_ERR_PIN);
} else if (result == -2) {
application.Alert(Lang::Strings::ERROR, Lang::Strings::REG_ERROR, "sad", Lang::Sounds::P3_ERR_REG);
} else {
break;
}
vTaskDelay(pdMS_TO_TICKS(10000));
}
// Print the ML307 modem information
std::string module_name = modem_.GetModuleName();
std::string imei = modem_.GetImei();
std::string iccid = modem_.GetIccid();
ESP_LOGI(TAG, "ML307 Module: %s", module_name.c_str());
ESP_LOGI(TAG, "ML307 IMEI: %s", imei.c_str());
ESP_LOGI(TAG, "ML307 ICCID: %s", iccid.c_str());
// Close all previous connections
modem_.ResetConnections();
// Enable sleep mode
modem_.SetSleepMode(true, 30);
}
Http* Ml307Board::CreateHttp() {
return new Ml307Http(modem_);
}
WebSocket* Ml307Board::CreateWebSocket() {
return new WebSocket(new Ml307SslTransport(modem_, 0));
}
Mqtt* Ml307Board::CreateMqtt() {
return new Ml307Mqtt(modem_, 0);
}
Udp* Ml307Board::CreateUdp() {
return new Ml307Udp(modem_, 0);
}
const char* Ml307Board::GetNetworkStateIcon() {
if (!modem_.network_ready()) {
return FONT_AWESOME_SIGNAL_OFF;
}
int csq = modem_.GetCsq();
if (csq == -1) {
return FONT_AWESOME_SIGNAL_OFF;
} else if (csq >= 0 && csq <= 14) {
return FONT_AWESOME_SIGNAL_1;
} else if (csq >= 15 && csq <= 19) {
return FONT_AWESOME_SIGNAL_2;
} else if (csq >= 20 && csq <= 24) {
return FONT_AWESOME_SIGNAL_3;
} else if (csq >= 25 && csq <= 31) {
return FONT_AWESOME_SIGNAL_4;
}
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_.GetModuleName() + "\",";
board_json += "\"carrier\":\"" + modem_.GetCarrierName() + "\",";
board_json += "\"csq\":\"" + std::to_string(modem_.GetCsq()) + "\",";
board_json += "\"imei\":\"" + modem_.GetImei() + "\",";
board_json += "\"iccid\":\"" + modem_.GetIccid() + "\",";
board_json += "\"cereg\":" + modem_.GetRegistrationState().ToString() + "}";
return board_json;
}
void Ml307Board::SetPowerSaveMode(bool enabled) {
// TODO: Implement power save mode for ML307
}
std::string Ml307Board::GetDeviceStatusJson() {
/*
* 返回设备状态JSON
*
* 返回的JSON结构如下:
* {
* "audio_speaker": {
* "volume": 70
* },
* "screen": {
* "brightness": 100,
* "theme": "light"
* },
* "battery": {
* "level": 50,
* "charging": true
* },
* "network": {
* "type": "cellular",
* "carrier": "CHINA MOBILE",
* "csq": 10
* }
* }
*/
auto& board = Board::GetInstance();
auto root = cJSON_CreateObject();
// Audio speaker
auto audio_speaker = cJSON_CreateObject();
auto audio_codec = board.GetAudioCodec();
if (audio_codec) {
cJSON_AddNumberToObject(audio_speaker, "volume", audio_codec->output_volume());
}
cJSON_AddItemToObject(root, "audio_speaker", audio_speaker);
// Screen brightness
auto backlight = board.GetBacklight();
auto screen = cJSON_CreateObject();
if (backlight) {
cJSON_AddNumberToObject(screen, "brightness", backlight->brightness());
}
auto display = board.GetDisplay();
if (display && display->height() > 64) { // For LCD display only
cJSON_AddStringToObject(screen, "theme", display->GetTheme().c_str());
}
cJSON_AddItemToObject(root, "screen", screen);
// Battery
int battery_level = 0;
bool charging = false;
bool discharging = false;
if (board.GetBatteryLevel(battery_level, charging, discharging)) {
cJSON* battery = cJSON_CreateObject();
cJSON_AddNumberToObject(battery, "level", battery_level);
cJSON_AddBoolToObject(battery, "charging", charging);
cJSON_AddItemToObject(root, "battery", battery);
}
// Network
auto network = cJSON_CreateObject();
cJSON_AddStringToObject(network, "type", "cellular");
cJSON_AddStringToObject(network, "carrier", modem_.GetCarrierName().c_str());
int csq = modem_.GetCsq();
if (csq == -1) {
cJSON_AddStringToObject(network, "signal", "unknown");
} else if (csq >= 0 && csq <= 14) {
cJSON_AddStringToObject(network, "signal", "very weak");
} else if (csq >= 15 && csq <= 19) {
cJSON_AddStringToObject(network, "signal", "weak");
} else if (csq >= 20 && csq <= 24) {
cJSON_AddStringToObject(network, "signal", "medium");
} else if (csq >= 25 && csq <= 31) {
cJSON_AddStringToObject(network, "signal", "strong");
}
cJSON_AddItemToObject(root, "network", network);
auto json_str = cJSON_PrintUnformatted(root);
std::string json(json_str);
cJSON_free(json_str);
cJSON_Delete(root);
return json;
}
@@ -0,0 +1,27 @@
#ifndef ML307_BOARD_H
#define ML307_BOARD_H
#include "board.h"
#include <ml307_at_modem.h>
class Ml307Board : public Board {
protected:
Ml307AtModem modem_;
virtual std::string GetBoardJson() override;
void WaitForNetworkReady();
public:
Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, size_t rx_buffer_size = 4096);
virtual std::string GetBoardType() override;
virtual void StartNetwork() override;
virtual Http* CreateHttp() override;
virtual WebSocket* CreateWebSocket() override;
virtual Mqtt* CreateMqtt() override;
virtual Udp* CreateUdp() override;
virtual const char* GetNetworkStateIcon() override;
virtual void SetPowerSaveMode(bool enabled) override;
virtual AudioCodec* GetAudioCodec() override { return nullptr; }
virtual std::string GetDeviceStatusJson() override;
};
#endif // ML307_BOARD_H
@@ -0,0 +1,103 @@
#include "power_save_timer.h"
#include "application.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_) {
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_) {
in_sleep_mode_ = true;
if (on_enter_sleep_mode_) {
on_enter_sleep_mode_();
}
if (cpu_max_freq_ != -1) {
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_) {
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);
}
if (on_exit_sleep_mode_) {
on_exit_sleep_mode_();
}
}
}
@@ -0,0 +1,33 @@
#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;
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_;
};
@@ -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);
}
@@ -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
@@ -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();
}
@@ -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
@@ -0,0 +1,274 @@
#include "wifi_board.h"
#include "display.h"
#include "application.h"
#include "system_info.h"
#include "font_awesome_symbols.h"
#include "settings.h"
#include "assets/lang_config.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_http.h>
#include <esp_mqtt.h>
#include <esp_udp.h>
#include <tcp_transport.h>
#include <tls_transport.h>
#include <web_socket.h>
#include <esp_log.h>
#include <wifi_station.h>
#include <wifi_configuration_ap.h>
#include <ssid_manager.h>
static const char *TAG = "WifiBoard";
WifiBoard::WifiBoard() {
Settings settings("wifi", true);
wifi_config_mode_ = settings.GetInt("force_ap") == 1;
if (wifi_config_mode_) {
ESP_LOGI(TAG, "force_ap is set to 1, reset to 0");
settings.SetInt("force_ap", 0);
}
}
std::string WifiBoard::GetBoardType() {
return "wifi";
}
void WifiBoard::EnterWifiConfigMode() {
auto& application = Application::GetInstance();
application.SetDeviceState(kDeviceStateWifiConfiguring);
auto& wifi_ap = WifiConfigurationAp::GetInstance();
wifi_ap.SetLanguage(Lang::CODE);
wifi_ap.SetSsidPrefix("Xiaozhi");
wifi_ap.Start();
// 显示 WiFi 配置 AP 的 SSID 和 Web 服务器 URL
std::string hint = Lang::Strings::CONNECT_TO_HOTSPOT;
hint += wifi_ap.GetSsid();
hint += Lang::Strings::ACCESS_VIA_BROWSER;
hint += wifi_ap.GetWebServerUrl();
hint += "\n\n";
// 播报配置 WiFi 的提示
application.Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "", Lang::Sounds::P3_WIFICONFIG);
// Wait forever until reset after configuration
while (true) {
int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
int min_free_sram = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL);
ESP_LOGI(TAG, "Free internal: %u minimal internal: %u", free_sram, min_free_sram);
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
void WifiBoard::StartNetwork() {
// User can press BOOT button while starting to enter WiFi configuration mode
if (wifi_config_mode_) {
EnterWifiConfigMode();
return;
}
// If no WiFi SSID is configured, enter WiFi configuration mode
auto& ssid_manager = SsidManager::GetInstance();
auto ssid_list = ssid_manager.GetSsidList();
if (ssid_list.empty()) {
wifi_config_mode_ = true;
EnterWifiConfigMode();
return;
}
auto& wifi_station = WifiStation::GetInstance();
wifi_station.OnScanBegin([this]() {
auto display = Board::GetInstance().GetDisplay();
display->ShowNotification(Lang::Strings::SCANNING_WIFI, 30000);
});
wifi_station.OnConnect([this](const std::string& ssid) {
auto display = Board::GetInstance().GetDisplay();
std::string notification = Lang::Strings::CONNECT_TO;
notification += ssid;
notification += "...";
display->ShowNotification(notification.c_str(), 30000);
});
wifi_station.OnConnected([this](const std::string& ssid) {
auto display = Board::GetInstance().GetDisplay();
std::string notification = Lang::Strings::CONNECTED_TO;
notification += ssid;
display->ShowNotification(notification.c_str(), 30000);
});
wifi_station.Start();
// Try to connect to WiFi, if failed, launch the WiFi configuration AP
if (!wifi_station.WaitForConnected(60 * 1000)) {
wifi_station.Stop();
wifi_config_mode_ = true;
EnterWifiConfigMode();
return;
}
}
Http* WifiBoard::CreateHttp() {
return new EspHttp();
}
WebSocket* WifiBoard::CreateWebSocket() {
Settings settings("websocket", false);
std::string url = settings.GetString("url");
if (url.find("wss://") == 0) {
return new WebSocket(new TlsTransport());
} else {
return new WebSocket(new TcpTransport());
}
return nullptr;
}
Mqtt* WifiBoard::CreateMqtt() {
return new EspMqtt();
}
Udp* WifiBoard::CreateUdp() {
return new EspUdp();
}
const char* WifiBoard::GetNetworkStateIcon() {
if (wifi_config_mode_) {
return FONT_AWESOME_WIFI;
}
auto& wifi_station = WifiStation::GetInstance();
if (!wifi_station.IsConnected()) {
return FONT_AWESOME_WIFI_OFF;
}
int8_t rssi = wifi_station.GetRssi();
if (rssi >= -60) {
return FONT_AWESOME_WIFI;
} else if (rssi >= -70) {
return FONT_AWESOME_WIFI_FAIR;
} else {
return FONT_AWESOME_WIFI_WEAK;
}
}
std::string WifiBoard::GetBoardJson() {
// Set the board type for OTA
auto& wifi_station = WifiStation::GetInstance();
std::string board_json = std::string("{\"type\":\"" BOARD_TYPE "\",");
board_json += "\"name\":\"" BOARD_NAME "\",";
if (!wifi_config_mode_) {
board_json += "\"ssid\":\"" + wifi_station.GetSsid() + "\",";
board_json += "\"rssi\":" + std::to_string(wifi_station.GetRssi()) + ",";
board_json += "\"channel\":" + std::to_string(wifi_station.GetChannel()) + ",";
board_json += "\"ip\":\"" + wifi_station.GetIpAddress() + "\",";
}
board_json += "\"mac\":\"" + SystemInfo::GetMacAddress() + "\"}";
return board_json;
}
void WifiBoard::SetPowerSaveMode(bool enabled) {
auto& wifi_station = WifiStation::GetInstance();
wifi_station.SetPowerSaveMode(enabled);
}
void WifiBoard::ResetWifiConfiguration() {
// Set a flag and reboot the device to enter the network configuration mode
{
Settings settings("wifi", true);
settings.SetInt("force_ap", 1);
}
GetDisplay()->ShowNotification(Lang::Strings::ENTERING_WIFI_CONFIG_MODE);
vTaskDelay(pdMS_TO_TICKS(1000));
// Reboot the device
esp_restart();
}
std::string WifiBoard::GetDeviceStatusJson() {
/*
* 返回设备状态JSON
*
* 返回的JSON结构如下:
* {
* "audio_speaker": {
* "volume": 70
* },
* "screen": {
* "brightness": 100,
* "theme": "light"
* },
* "battery": {
* "level": 50,
* "charging": true
* },
* "network": {
* "type": "wifi",
* "ssid": "Xiaozhi",
* "rssi": -60
* },
* "chip": {
* "temperature": 25
* }
* }
*/
auto& board = Board::GetInstance();
auto root = cJSON_CreateObject();
// Audio speaker
auto audio_speaker = cJSON_CreateObject();
auto audio_codec = board.GetAudioCodec();
if (audio_codec) {
cJSON_AddNumberToObject(audio_speaker, "volume", audio_codec->output_volume());
}
cJSON_AddItemToObject(root, "audio_speaker", audio_speaker);
// Screen brightness
auto backlight = board.GetBacklight();
auto screen = cJSON_CreateObject();
if (backlight) {
cJSON_AddNumberToObject(screen, "brightness", backlight->brightness());
}
auto display = board.GetDisplay();
if (display && display->height() > 64) { // For LCD display only
cJSON_AddStringToObject(screen, "theme", display->GetTheme().c_str());
}
cJSON_AddItemToObject(root, "screen", screen);
// Battery
int battery_level = 0;
bool charging = false;
bool discharging = false;
if (board.GetBatteryLevel(battery_level, charging, discharging)) {
cJSON* battery = cJSON_CreateObject();
cJSON_AddNumberToObject(battery, "level", battery_level);
cJSON_AddBoolToObject(battery, "charging", charging);
cJSON_AddItemToObject(root, "battery", battery);
}
// Network
auto network = cJSON_CreateObject();
auto& wifi_station = WifiStation::GetInstance();
cJSON_AddStringToObject(network, "type", "wifi");
cJSON_AddStringToObject(network, "ssid", wifi_station.GetSsid().c_str());
int rssi = wifi_station.GetRssi();
if (rssi >= -60) {
cJSON_AddStringToObject(network, "signal", "strong");
} else if (rssi >= -70) {
cJSON_AddStringToObject(network, "signal", "medium");
} else {
cJSON_AddStringToObject(network, "signal", "weak");
}
cJSON_AddItemToObject(root, "network", network);
// Chip
float esp32temp = 0.0f;
if (board.GetTemperature(esp32temp)) {
auto chip = cJSON_CreateObject();
cJSON_AddNumberToObject(chip, "temperature", esp32temp);
cJSON_AddItemToObject(root, "chip", chip);
}
auto json_str = cJSON_PrintUnformatted(root);
std::string json(json_str);
cJSON_free(json_str);
cJSON_Delete(root);
return json;
}
@@ -0,0 +1,27 @@
#ifndef WIFI_BOARD_H
#define WIFI_BOARD_H
#include "board.h"
class WifiBoard : public Board {
protected:
bool wifi_config_mode_ = false;
void EnterWifiConfigMode();
virtual std::string GetBoardJson() override;
public:
WifiBoard();
virtual std::string GetBoardType() override;
virtual void StartNetwork() override;
virtual Http* CreateHttp() override;
virtual WebSocket* CreateWebSocket() override;
virtual Mqtt* CreateMqtt() override;
virtual Udp* CreateUdp() override;
virtual const char* GetNetworkStateIcon() override;
virtual void SetPowerSaveMode(bool enabled) override;
virtual void ResetWifiConfiguration();
virtual AudioCodec* GetAudioCodec() override { return nullptr; }
virtual std::string GetDeviceStatusJson() override;
};
#endif // WIFI_BOARD_H