feat: 小智云聊增加两个语音指令 (#1596)
Some checks failed
Build Boards / Determine variants to build (push) Has been cancelled
Build Boards / Build ${{ matrix.name }} (push) Has been cancelled

This commit is contained in:
zczc365 2025-12-26 03:01:18 +08:00 committed by GitHub
parent 5113a5f4bb
commit 5d44633687
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 145 additions and 92 deletions

View File

@ -1,46 +1,56 @@
# 小智云聊S3
# 小智云聊 S3
## 简介
小智云聊S3是小智AI的魔改项目是首个2.8寸护眼大屏+大字体+2000mah大电池的量产成品做了大量创新和优化。
小智云聊 S3 是小智 AI 的魔改项目,是首个 2.8 寸护眼大屏+大字体+2000mah 大电池的量产成品,做了大量创新和优化。
## 合并版
合并版代码在小智AI主项目中维护跟随主项目的一起版本更新便于用户自行扩展和第三方固件扩展。支持语音唤醒、语音打断、OTA、4G自由切换等功能。
>### 按键操作
>- **开机**: 关机状态长按1秒后释放按键自动开机
>- **关机**: 开机状态长按1秒后释放按键标题栏会显示'请稍候'再等2秒自动关机
>- **唤醒/打断**: 正常通话环境下,单击按键
>- **切换4G/Wifi**: 启动过程或者配网界面1秒钟内双击按键需安装4G模块
>- **重新配网**: 开机状态1秒钟内三击按键会自动重启并进入配网界面
合并版代码在小智 AI 主项目中维护跟随主项目的一起版本更新便于用户自行扩展和第三方固件扩展。支持语音唤醒、语音打断、OTA、4G 自由切换等功能。
> ### 按键操作
>
> - **开机**: 关机状态,长按 1 秒后释放按键,自动开机。
> - **关机**: 开机状态,长按 1 秒后释放按键,标题栏会显示'请稍候',再等 2 秒自动关机。
> - **唤醒/打断**: 正常通话环境下,单击按键。
> - **切换 4G/Wifi**: 启动过程或者配网界面1 秒钟内双击按键(需安装 4G 模块)。
> - **重新配网**: 开机状态1 秒钟内三击按键,会自动重启并进入配网界面。
> ### 语音指令
>
> - **打开/关闭语音打断模式**: 在播放音乐时,需要关闭语音打断模式,否则可能会打断音乐播放。
> - **切换 IPS 屏幕显示模式**: 新版小智云聊 S3 升级了 IPS 屏幕,需要切换屏幕显示模式后才能正常显示,可以来回切换。
## 魔改版
魔改版由于底层改动太大,代码单独维护,定期合并主项目代码。
>### 为什么是魔改
>- 首个实现微信二维码配网。
>- 首个支持单手机配网。
>- 首个支持扫二维码访问控制台。
>- 首发支持繁体、日文、英文版界面
>- 首个全语音操控模式
>- 独家提供一键刷机脚本等多种刷机方式
> ### 为什么是魔改
>
> - 首个实现微信二维码配网。
> - 首个支持单手机配网。
> - 首个支持扫二维码访问控制台。
> - 首发支持繁体、日文、英文版界面。
> - 首个全语音操控模式。
> - 独家提供一键刷机脚本等多种刷机方式。
## 版本区别
>| 特性 | 合并版 | 魔改版 |
>| --- | --- | --- |
>| 语音打断 | ✓ | ✓ |
>| 4G功能 | ✓ | ✓ |
>| 自动更新固件 | ✓ | X |
>| 第三方固件支持 | ✓ | X |
>| 天气待机界面 | X | ✓ |
>| 闹钟提醒 | X | ✓ |
>| 网络音乐播放 | X | ✓ |
>| 微信扫码配网 | X | ✓ |
>| 单手机配网 | X | ✓ |
>| 扫码访问控制台 | X | ✓ |
>| 繁日英文界面 | X | ✓ |
>| 多语言支持 | X | ✓ |
>| 外接蓝牙音箱 | X | ✓ |
> | 特性 | 合并版 | 魔改版 |
> | -------------- | ------ | ------ |
> | 语音打断 | ✓ | ✓ |
> | 4G 功能 | ✓ | ✓ |
> | 自动更新固件 | ✓ | X |
> | 第三方固件支持 | ✓ | X |
> | 天气待机界面 | X | ✓ |
> | 闹钟提醒 | X | ✓ |
> | 网络音乐播放 | X | ✓ |
> | 微信扫码配网 | X | ✓ |
> | 单手机配网 | X | ✓ |
> | 扫码访问控制台 | X | ✓ |
> | 繁日英文界面 | X | ✓ |
> | 多语言支持 | X | ✓ |
> | 外接蓝牙音箱 | X | ✓ |
# 编译配置命令
@ -85,4 +95,3 @@ idf.py build
```bash
idf.py build flash monitor
```

View File

@ -1,21 +1,22 @@
#include "lvgl_theme.h"
#include "dual_network_board.h"
#include "codecs/es8388_audio_codec.h"
#include "display/lcd_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "power_save_timer.h"
#include "power_manager.h"
#include "assets/lang_config.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <esp_log.h>
#include "settings.h"
#include "application.h"
#include "assets/lang_config.h"
#include "button.h"
#include "codecs/es8388_audio_codec.h"
#include "config.h"
#include "display/lcd_display.h"
#include "dual_network_board.h"
#include "lvgl_theme.h"
#include "power_manager.h"
#include "power_save_timer.h"
#include "mcp_server.h"
#define TAG "YunliaoS3"
class YunliaoS3 : public DualNetworkBoard {
private:
private:
i2c_master_bus_handle_t codec_i2c_bus_;
Button boot_button_;
SpiLcdDisplay* display_;
@ -49,9 +50,10 @@ private:
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
.flags =
{
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_));
}
@ -63,8 +65,10 @@ private:
buscfg.sclk_io_num = DISPLAY_SPI_PIN_SCLK;
buscfg.quadwp_io_num = GPIO_NUM_NC;
buscfg.quadhd_io_num = GPIO_NUM_NC;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(spi_bus_initialize(DISPLAY_SPI_LCD_HOST, &buscfg, SPI_DMA_CH_AUTO));
buscfg.max_transfer_sz =
DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(
spi_bus_initialize(DISPLAY_SPI_LCD_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
void InitializeButtons() {
@ -76,23 +80,26 @@ private:
boot_button_.OnDoubleClick([this]() {
ESP_LOGI(TAG, "Button OnDoubleClick");
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
if (app.GetDeviceState() == kDeviceStateStarting ||
app.GetDeviceState() == kDeviceStateWifiConfiguring) {
SwitchNetworkType();
}
});
boot_button_.OnMultipleClick([this]() {
ESP_LOGI(TAG, "Button OnThreeClick");
if (GetNetworkType() == NetworkType::WIFI) {
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
wifi_board.EnterWifiConfigMode();
}
},3);
});
boot_button_.OnMultipleClick(
[this]() {
ESP_LOGI(TAG, "Button OnThreeClick");
if (GetNetworkType() == NetworkType::WIFI) {
auto& wifi_board =
static_cast<WifiBoard&>(GetCurrentBoard());
wifi_board.EnterWifiConfigMode();
}
}, 3);
boot_button_.OnLongPress([this]() {
ESP_LOGI(TAG, "Button LongPress to Sleep");
display_->SetStatus(Lang::Strings::PLEASE_WAIT);
vTaskDelay(pdMS_TO_TICKS(2000));
power_manager_->Sleep();
});
});
}
void InitializeSt7789Display() {
esp_lcd_panel_io_handle_t panel_io = nullptr;
@ -107,19 +114,23 @@ private:
io_config.trans_queue_depth = 10;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(DISPLAY_SPI_LCD_HOST, &io_config, &panel_io));
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(DISPLAY_SPI_LCD_HOST,
&io_config, &panel_io));
// 初始化液晶屏驱动芯片ST7789
ESP_LOGD(TAG, "Install LCD driver");
Settings settings("display", false);
bool currentIpsMode = settings.GetBool("ips_mode", DISPLAY_INVERT_COLOR);
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = DISPLAY_SPI_PIN_LCD_RST;
panel_config.rgb_ele_order = DISPLAY_RGB_ORDER_COLOR;
panel_config.bits_per_pixel = 16;
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
ESP_ERROR_CHECK(
esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, DISPLAY_INVERT_COLOR);
esp_lcd_panel_invert_color(panel, currentIpsMode);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
display_ = new SpiLcdDisplay(panel_io, panel, DISPLAY_WIDTH,
@ -132,12 +143,50 @@ private:
display_->SetTheme(theme);
}
}
void InitializeTools(){
auto& mcp_server = McpServer::GetInstance();
mcp_server.AddTool("self.system.set_aec",
"Enable or disable voice interruption mode (AEC:Acoustic Echo Cancellation). When enabled, the device can detect voice interruptions and respond accordingly.",
PropertyList({
Property("enable", kPropertyTypeBoolean)
}), [this](const PropertyList& properties) {
bool enable = properties["enable"].value<bool>();
SetAecMode(enable);
return true;
});
public:
YunliaoS3() :
DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, GPIO_NUM_NC, 0),
boot_button_(BOOT_BUTTON_PIN),
power_manager_(new PowerManager()){
mcp_server.AddTool("self.system.switch_TFT",
"Switch TFT display mode between normal and inverted colors. This will toggle the IPS mode and reboot the device.",
PropertyList(), [this](const PropertyList& properties) {
SwitchTFT();
return true;
});
}
void SetAecMode(bool enable) {
AecMode newMode = enable ? kAecOnDeviceSide : kAecOff;
auto& app = Application::GetInstance();
app.StopListening();
app.SetDeviceState(kDeviceStateIdle);
app.SetAecMode(newMode);
Settings settings("aec", true);
settings.SetInt("mode", newMode);
}
void SwitchTFT() {
Settings settings("display", true);
bool currentIpsMode = settings.GetBool("ips_mode", false);
settings.SetBool("ips_mode", !currentIpsMode);
ESP_LOGI(TAG, "IPS mode toggled to %s", !currentIpsMode ? "enabled" : "disabled");
vTaskDelay(pdMS_TO_TICKS(1000));
auto& app = Application::GetInstance();
app.Reboot();
}
public:
YunliaoS3()
: DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, GPIO_NUM_NC, 0),
boot_button_(BOOT_BUTTON_PIN),
power_manager_(new PowerManager()) {
power_manager_->Start5V();
power_manager_->Initialize();
InitializeI2c();
@ -146,7 +195,7 @@ public:
InitializeSpi();
InitializeSt7789Display();
power_manager_->OnChargingStatusDisChanged([this](bool is_discharging) {
if(power_save_timer_){
if (power_save_timer_) {
if (is_discharging) {
power_save_timer_->SetEnabled(true);
} else {
@ -154,46 +203,41 @@ public:
}
}
});
if(GetNetworkType() == NetworkType::WIFI){
if (GetNetworkType() == NetworkType::WIFI) {
power_manager_->Shutdown4G();
}else{
} else {
power_manager_->Start4G();
}
GetBacklight()->RestoreBrightness();
while(gpio_get_level(BOOT_BUTTON_PIN) == 0){
while (gpio_get_level(BOOT_BUTTON_PIN) == 0) {
vTaskDelay(pdMS_TO_TICKS(10));
}
InitializeButtons();
Settings settings("aec", false);
auto& app = Application::GetInstance();
app.SetAecMode(settings.GetInt("mode",kAecOnDeviceSide) == kAecOnDeviceSide ? kAecOnDeviceSide : kAecOff);
InitializeTools();
}
virtual AudioCodec* GetAudioCodec() override {
static Es8388AudioCodec audio_codec(
codec_i2c_bus_,
I2C_NUM_0,
AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK,
AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN,
AUDIO_CODEC_PA_PIN,
AUDIO_CODEC_ES8388_ADDR,
AUDIO_INPUT_REFERENCE
);
codec_i2c_bus_, I2C_NUM_0, AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE, AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN,
AUDIO_CODEC_PA_PIN, AUDIO_CODEC_ES8388_ADDR, AUDIO_INPUT_REFERENCE);
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
virtual Display* GetDisplay() override { return display_; }
virtual Backlight* GetBacklight() override {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN,
DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight;
}
virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override {
virtual bool GetBatteryLevel(int& level, bool& charging,
bool& discharging) override {
level = power_manager_->GetBatteryLevel();
charging = power_manager_->IsCharging();
discharging = power_manager_->IsDischarging();