Compare commits

...

62 Commits

Author SHA1 Message Date
Terrence
9e96f0f027 Refactor listening mode handling and wake word detection configuration
- Replace direct mode setting logic with a new GetDefaultListeningMode method for improved clarity and maintainability.
- Update HandleToggleChatEvent, HandleWakeWordDetectedEvent, and ContinueWakeWordInvoke to utilize the new method for determining listening mode.
- Introduce Kconfig option WAKE_WORD_DETECTION_IN_LISTENING to enable or disable wake word detection during listening mode, enhancing configurability.
2026-02-04 12:28:59 +08:00
Terrence
f39c112970 Refactor audio processing to enhance thread safety and state management
- Implement early return checks in Feed methods of AfeAudioProcessor, AfeWakeWord, CustomWakeWord, and EspWakeWord to prevent processing when not running.
- Introduce std::atomic for running state in CustomWakeWord and EspWakeWord to ensure thread-safe access.
- Consolidate input buffer management with mutex locks to avoid race conditions during Stop and Feed operations.
2026-02-04 12:17:16 +08:00
Terrence
f7e258979e Enhance audio processing and wake word detection
- Set task priority in Application::Run to improve responsiveness.
- Log detected wake words with their state in HandleWakeWordDetectedEvent.
- Streamline audio feeding in AudioService to handle both wake word and audio processor events.
- Implement input buffering in AfeAudioProcessor, AfeWakeWord, CustomWakeWord, and EspWakeWord to manage audio data more efficiently.
- Clear input buffers on stop to prevent residual data issues.
2026-02-04 12:04:19 +08:00
Xiaoxia
37110a9d05 Fix: esp32camera pixel byte order and uart-uhci compiling error (#1728)
* Fix: uart-uhci compiling errors

* Enhance Esp32Camera functionality by adding optional byte swapping for RGB565 format. Introduce SetSwapBytes method to enable/disable byte order swapping, and update Capture method to utilize an encode buffer for improved memory management and performance during image processing.
2026-02-02 15:33:32 +08:00
小鹏
796312db4c Enhance Otto Robot camera support by adding configuration for OV3660. (#1726) 2026-02-02 10:22:53 +08:00
tkpdx01
9e1724e892 feat: add M5Stack Cardputer Adv board support (#1718)
Add support for M5Stack Cardputer Adv, a card-sized computer based on
ESP32-S3FN8 (Stamp-S3A) with the following features:

Hardware:
- MCU: ESP32-S3FN8 @ 240MHz, 8MB Flash (no PSRAM)
- Display: ST7789V2 1.14" 240x135
- Audio: ES8311 codec + NS4150B amplifier
- Keyboard: 56-key via TCA8418
- IMU: BMI270

Key configurations:
- SPI3_HOST with 3-wire SPI mode for display
- 256Hz PWM frequency for backlight (matching M5GFX)
- ES8311 with use_mclk=false (no MCLK pin)
- Display offset X=40, Y=52 for correct alignment

新增 M5Stack Cardputer Adv 开发板支持

支持基于 ESP32-S3FN8 (Stamp-S3A) 的卡片式电脑 M5Stack Cardputer Adv:

硬件规格:
- MCU: ESP32-S3FN8 @ 240MHz, 8MB Flash (无 PSRAM)
- 显示屏: ST7789V2 1.14" 240x135
- 音频: ES8311 编解码器 + NS4150B 功放
- 键盘: 56键 (TCA8418)
- IMU: BMI270

关键配置:
- 显示使用 SPI3_HOST 和 3-wire SPI 模式
- 背光 PWM 频率 256Hz (与 M5GFX 一致)
- ES8311 设置 use_mclk=false (无 MCLK 引脚)
- 显示偏移 X=40, Y=52 以正确对齐
2026-02-02 10:01:36 +08:00
Xiaoxia
0b3b98eca7 Update esp-ml307 component version to 3.6.2 to support UART DMA (#1724)
* Update esp-ml307 dependency version to ~3.6.0 in idf_component.yml

* Update .gitignore to include 'dist/' directory, add ml307 and dual_network_board source files to CMakeLists.txt, and update esp-ml307 dependency version to ~3.6.2 in idf_component.yml. Refactor CompactWifiBoard and CompactWifiBoardLCD classes to inherit from WifiBoard instead of DualNetworkBoard, simplifying network handling logic.
2026-02-02 09:53:06 +08:00
Xiaoxia
abd62648cb Implement early return in AfeWakeWord::Feed to prevent processing when detection is not running. This enhances the robustness of the wake word detection logic. (#1723) 2026-02-01 17:42:34 +08:00
Xiaoxia
0883a36537 Refactor audio channel handling and wake word detection in Application class (#1722)
- Introduced ContinueOpenAudioChannel and ContinueWakeWordInvoke methods to streamline audio channel management and wake word processing.
- Updated HandleToggleChatEvent and HandleWakeWordDetectedEvent to utilize scheduling for state changes, improving UI responsiveness.
- Simplified logic for setting listening modes based on audio channel state, enhancing code clarity and maintainability.
2026-02-01 14:55:47 +08:00
Xiaoxia
b6c61fe390 Update project version to 2.2.2, Noto fonts and emoji support. (#1720) 2026-02-01 01:04:24 +08:00
Xiaoxia
f7284a57df Enhance memory management in asset download and OTA processes by repl… (#1716)
* Enhance memory management in asset download and OTA processes by replacing static buffer allocations with dynamic memory allocation using heap capabilities. Update SPIRAM configuration values for improved memory usage. Add logging for error handling in buffer allocation failures. Introduce a new parameter in CloseAudioChannel to control goodbye message sending in MQTT and WebSocket protocols.

* Update component versions in idf_component.yml and refactor GIF decoder functions for improved performance. Bump versions for audio effects, audio codec, LED strip, and other dependencies. Change GIF read and seek functions to inline for optimization.

* Update language files to include new phrases for flight mode and connection status across multiple locales. Added translations for "FLIGHT_MODE_ON", "FLIGHT_MODE_OFF", "CONNECTION_SUCCESSFUL", and "MODEM_INIT_ERROR" in various languages, enhancing user experience and localization support.

* fix wechat display
2026-01-31 22:58:08 +08:00
小鹏
96f34ec70f Refactor emoji initialization for Electron and Otto boards to use Assets system (#1704)
* otto v1.4.0 MCP

1.使用MCP协议控制机器人
2.gif继承lcdDisplay,避免修改lcdDisplay

* otto v1.4.1 gif as components

gif as components

* electronBot v1.1.0 mcp

1.增加electronBot支持
2.mcp协议
3.gif 作为组件
4.display子类

* 规范代码

1.规范代码
2.修复切换主题死机bug

* fix(ota): 修复 ottoRobot和electronBot OTA 升级崩溃问题 bug

* 1.增加robot舵机初始位置校准
2.fix(mcp_sever) 超出范围异常捕获类型  bug

* refactor: Update Electron and Otto emoji display implementations

- Removed GIF selection from Kconfig for Electron and Otto boards.
- Updated Electron and Otto bot versions to 2.0.4 in their respective config files.
- Refactored emoji display classes to utilize EmojiCollection for managing emojis.
- Enhanced chat label setup and status display functionality in both classes.
- Cleaned up unused code and improved initialization logging for emoji displays.

* Rename OTTO_ICON_FONT.c to otto_icon_font.c

* Rename OTTO_ICON_FONT.c to otto_icon_font.c

* refactor: Update Otto emoji display configurations and functionalities

- Changed chat label text mode to circular scrolling for both Otto and Electron emoji displays.
- Bumped Otto robot version to 2.0.5 in the configuration file.
- Added new actions for Otto robot including Sit, WhirlwindLeg, Fitness, Greeting, Shy, RadioCalisthenics, MagicCircle, and Showcase.
- Enhanced servo sequence handling and added support for executing custom servo sequences.
- Improved logging and error handling for servo sequence execution.

* refactor: Update chat label long mode for Electron and Otto emoji displays

- Changed chat label text mode from wrap to circular scrolling for both Electron and Otto emoji displays.
- Improved consistency in chat label setup across both implementations.

* Update Otto robot README with new actions and parameters

* Update Otto controller parameters for oscillation settings

- Changed default oscillation period from 500ms to 300ms.
- Increased default steps from 5.0 to 8.0.
- Updated default amplitude from 20 degrees to 0 degrees.
- Enhanced documentation with new examples for oscillation modes and sequences.

* Fix default amplitude initialization in Otto controller to use a single zero instead of two digits.

* chore: update txp666/otto-emoji-gif-component version to 1.0.3 in idf_component.yml

* Refactor Otto controller
- Consolidated movement actions into a unified tool for the Otto robot, allowing for a single action command with various parameters.
- Removed individual movement tools (walk, turn, jump, etc.) and replaced them with a more flexible action system.

* Enhance Otto robot functionality by adding WebSocket control server and IP address retrieval feature. Updated config to support WebSocket, and revised README to include new control options and usage examples.

* Add camera support for Otto Robot board

- Introduced configuration option to enable the Otto Robot camera in Kconfig.
- Updated config.h to define camera-related GPIO pins and settings.
- Modified config.json to include camera configuration.
- Enhanced otto_robot.cc to initialize I2C and camera components when the camera is enabled.
- Adjusted power_manager.h to manage battery updates during camera operations.
- Removed unused SetupChatLabel method from OttoEmojiDisplay class.

* Refactor Otto Robot configuration and initialization

- Removed the camera configuration option from Kconfig and related code.
- Introduced a new HardwareConfig struct to encapsulate hardware pin definitions and settings.
- Updated config.h to define camera and non-camera configurations using the new struct.
- Refactored otto_controller.cc and otto_robot.cc to utilize the HardwareConfig struct for initialization.
- Enhanced camera detection and initialization logic based on hardware version.
- Improved audio codec initialization based on configuration settings.

* Refactor emoji initialization for Electron and Otto boards to use Assets system

- Removed direct emoji initialization from `InitializeElectronEmojis` and `InitializeOttoEmojis` methods, delegating the responsibility to the Assets system.
- Updated `CMakeLists.txt` to set `DEFAULT_EMOJI_COLLECTION` to `otto-gif` for both boards.
- Enhanced `build_default_assets.py` to support alias mapping for Otto GIF emojis.
- Updated `idf_component.yml` to bump `otto-emoji-gif-component` version to `^1.0.5` for improved functionality.
2026-01-31 18:13:15 +08:00
Xiaoxia
aad2f60b87 fix: reset esp-box-3 display to lvgl (#1715) 2026-01-31 03:14:50 +08:00
Copilot
5b874bc3ad Fix LVGL object deletion crash and thread safety issues in SetChatMessage (#1708)
* Initial plan

* Fix LVGL object deletion issue in SetChatMessage

- Refresh child_count after deleting first_child to avoid using stale count
- Add lv_obj_is_valid() checks before accessing deleted objects
- Prevent duplicate deletion by refreshing child_count before system message deletion
- Ensures last_child validation before scrolling to it

Co-authored-by: 78 <4488133+78@users.noreply.github.com>

* Address code review feedback on LVGL object deletion fix

- Move last_child retrieval after deletion to avoid stale pointer
- Add child_count > 0 check before scrolling to prevent edge case issues
- Add lv_obj_is_valid() check for last_bubble for consistency

Co-authored-by: 78 <4488133+78@users.noreply.github.com>

* Final review: LVGL object deletion fix complete

Co-authored-by: 78 <4488133+78@users.noreply.github.com>

* Remove std::thread wrappers in display callbacks to fix thread safety

Remove detached threads that were calling SetChatMessage from download/upgrade
progress callbacks. The DisplayLockGuard mutex already provides thread safety,
so we can call display methods directly from the callback thread without
spawning additional threads, which were causing race conditions.

Co-authored-by: 78 <4488133+78@users.noreply.github.com>

* Use Application::Schedule() for display updates in callbacks

Instead of calling SetChatMessage directly from download/upgrade progress
callbacks, queue the updates using Application::Schedule(). This prevents
blocking the network receive task and avoids potential UART FIFO overflow
issues with 4G modems. The scheduled callbacks execute in the main task
thread, maintaining proper thread safety.

Co-authored-by: 78 <4488133+78@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: 78 <4488133+78@users.noreply.github.com>
2026-01-31 00:11:42 +08:00
ws-hsw
7d515a8345 add esp32-s3-touch-lcd-4.3c (#1691)
* add esp32-s3-touch-lcd-4.3c

Add clang-format (#1694)

-文件.clang-format配置基于Google C++风格指南,并做了适当调整
1.使用4空格缩进
2.行宽限制为100字符
3.采用风格的大括号和空格规则

-编写code_style.m
1.针对不同开发平台(windows/Linux/macOs)
2.举例进行整体项目格式化(建议owner择机进行)
3.举例单文件格式化、格式检查
4.举例如何跳过特定代码段的格式化(待补充指定跳过文件格式化)
5.IDE集成说明

Co-authored-by: CaptainBarker <1350989027@qq.com>

add 征辰科技 AI camera wifi board and 4G board(26 Resubmit) (#1689)

add esp32-s3-touch-lcd-4.3c

* add esp32-s3-touch-lcd-4.3c

* add esp32-s3-touch-lcd-4.3c

* add esp32-s3-touch-lcd-4.3c
2026-01-30 01:08:05 +08:00
Xiaoxia
d9f7682b2d Fix RNDIS board and enhance camera initialization (#1702)
* Add support for RNDIS board and enhance camera initialization

- Included rndis_board.cc in the build for ESP32S3 and ESP32P4 targets.
- Updated camera initialization logic in esp32s3_korvo2_v3_board.cc and esp32s3_korvo2_v3_board.cc to use a more structured camera_config_t setup.
- Improved code readability by refining comments and formatting in the camera initialization functions.

* Remove outdated camera configuration options from esp32s3-korvo2-v3-rndis config.json to streamline setup and improve clarity.

* Update IDF version in build configuration and component dependencies to v5.5.2 for improved compatibility.

* update discord links

---------

Co-authored-by: Xiaoxia <terrence.huang@tenclass.com>
2026-01-28 16:11:26 +08:00
Wang is proud
f6ca040d19 Change the Bluetooth device name to "Xiaozhi-Blufi" in blufi provisioning. (#1701) 2026-01-28 01:51:42 +08:00
ZhouKe
7ad22d49af Support USB RNDIS (#1655)
* add rndis board support

* 增加代码兼容性处理

* 再次修复代码兼容性问题

* rename board

---------

Co-authored-by: zk <982145@qq.com>
2026-01-27 19:19:44 +08:00
HonestQiao
7fa9056527 Support both ov2640 and ov3660 with esp_video (#1695)
* Support both ov2640 and ov3660 with esp_video

* Use configuration to set up the use of cameras ov3660 and ov2640
2026-01-27 19:12:23 +08:00
Taikoto
62b93f986f add 征辰科技 AI camera wifi board and 4G board(26 Resubmit) (#1689) 2026-01-26 20:45:18 +08:00
Wang is proud
b9617368a0 Add clang-format (#1694)
-文件.clang-format配置基于Google C++风格指南,并做了适当调整
1.使用4空格缩进
2.行宽限制为100字符
3.采用风格的大括号和空格规则

-编写code_style.m
1.针对不同开发平台(windows/Linux/macOs)
2.举例进行整体项目格式化(建议owner择机进行)
3.举例单文件格式化、格式检查
4.举例如何跳过特定代码段的格式化(待补充指定跳过文件格式化)
5.IDE集成说明

Co-authored-by: CaptainBarker <1350989027@qq.com>
2026-01-26 20:42:31 +08:00
Wang is proud
b217cddbd8 Add an open-source server-related project to the document. (#1688) 2026-01-24 22:01:14 +08:00
Wang is proud
3a52761d30 Fixed compilation errors that occurred after enabling blufi (#1677) 2026-01-24 21:55:24 +08:00
Wang is proud
6b3659c2f5 Fixed an issue where the tail of the sound might be truncated in auto mode. (#1675) 2026-01-24 15:50:46 +08:00
Xiaoxia
734b5b410a Support both esp_video and esp32_camera (#1671)
* Update project version to 2.2.1 and refactor camera component handling

- Incremented project version from 2.2.0 to 2.2.1 in CMakeLists.txt.
- Removed legacy esp32_camera component and replaced it with esp_video for ESP32-S3 and ESP32-P4 boards.
- Updated board implementations to utilize the new esp_video component, ensuring compatibility and improved functionality.
- Cleaned up Kconfig options related to camera selection, streamlining the configuration process.
- Enhanced camera initialization logic across various board files to support the new component structure.

* Refactor camera handling in AtomS3R CAM/M12 EchoBase board

- Replaced the legacy EspVideo component with the new Esp32Camera class for improved camera functionality.
- Updated camera initialization logic to utilize a more structured configuration approach, enhancing clarity and maintainability.
- Removed outdated comments and code related to the previous camera implementation in the README file.

* Update camera configuration for atoms3r-cam-m12-echo-base

- Removed outdated camera configuration options from config.json to streamline the setup.
- Retained essential partition table configuration for improved clarity.

* Enhance Esp32Camera functionality and memory management

- Added esp_timer.h for improved timing functionality.
- Streamlined camera initialization by removing redundant frame buffer setup and logging.
- Improved memory allocation for JPEG encoding and added error handling for unsupported pixel formats.
- Updated comments for clarity and consistency, ensuring better understanding of the code flow.
2026-01-20 22:44:37 +08:00
Kevincoooool
d5ec8f7081 Add ESP32-S3 camera component selection and support (#1670)
Introduces a new esp32s3_camera implementation for ESP32-S3 boards using the esp_camera component, with conditional compilation and Kconfig options to select between esp_camera and esp_video. Updates board initialization code and config files to use the new camera class where appropriate, and adjusts build system and dependencies to support both camera components on ESP32-S3 and ESP32-P4 targets.
2026-01-20 19:56:36 +08:00
Xiaoxia
89674f8838 v2.2.0: Add bread-compact-nt26 board (#1663)
* Refactor application error handling and improve network task logic

- Updated error handling for modem initialization failure in Application::Initialize().
- Added new error message for modem initialization in English and Chinese language files.
- Simplified lambda captures in NetworkTask to avoid unnecessary references.
- Set main task priority in Application::Run() for better performance.

* Add support for Bread Compact NT26 board

- Introduced new board configuration for Bread Compact NT26 in CMakeLists.txt and Kconfig.
- Added board-specific implementation in compact_nt26_board.cc and nt26_board.cc.
- Created configuration files for NT26, including config.h and config.json.
- Updated dependencies in idf_component.yml to include uart-eth-modem.
- Translated error messages in config.h for OLED display type selection to English.
- Enhanced display and button initialization logic for NT26 board.

* Update project version and improve build configuration

- Updated project version from 2.1.0 to 2.2.0 in CMakeLists.txt.
- Enabled minimal build configuration to include only essential components.
- Updated README files to replace QQ group links with Discord links for community engagement.

* Update Bread Compact NT26 board configuration name in config.json

* fix compile errors

* Update uart-eth-modem dependency format in idf_component.yml

* fix esp32 compiling errors

* Update CMakeLists.txt to change component dependency from REQUIRES to PRIV_REQUIRES for esp_pm, esp_psram, and esp_driver_gpio

* Refactor CMakeLists.txt to explicitly list board common source files and update include directories for better clarity and organization.

* Add esp_driver_ppa as a dependency in CMakeLists.txt
2026-01-19 21:46:21 +08:00
Tomato Me
ed51705240 add waveshare esp32-s3-rlcd-4.2 (#1639) 2026-01-15 19:14:42 +08:00
Y1hsiaochunnn
c9fa5fabc5 feat: New support for third-party boards: waveshare ESP32-C6-Touch-AMOLED-1.8 (#1656)
* content: New support for third-party boards: waveshare ESP32-C6-Touch-AMOLED-1.8

* content: Change the TAG of ESP32-C6-Touch-AMOLED-1.8

* content: Change the config.json of ESP32-C6-Touch-AMOLED-1.8
2026-01-15 19:10:28 +08:00
YeezB
e3ed350b8b Fix K10 compile doc error (#1654) 2026-01-15 01:12:31 +08:00
唐杰
d963e120db Fix abnormal screen and camera display on DF K10 when running on Box (#1648)
* 解决屏幕和摄像头无法初始化问题

* 修改摄像头时钟

* change readme

* chang config
2026-01-14 17:11:25 +08:00
Xiaoxia
2ff3796289 Enhance audio service: Add mutex for input resampler and reset logic to prevent buffer overflow during mode switching (#1653) 2026-01-14 02:17:41 +08:00
顿玄
b48506171b 修复ESP-Hi报错 (#1643)
* Update esp_hi.cc

* Remove error check for event loop creation

If event loop already created, this function returns ESP_ERR_INVALID_STATE

---------

Co-authored-by: Xiaoxia <terrence@tenclass.com>
2026-01-10 16:29:12 +08:00
espressif2022
7240ea99f1 Fixed build error with IDF v5.4 (#1637) 2026-01-09 17:19:06 +08:00
espressif2022
1e8fefbede feat: update emote display (#1629) 2026-01-07 20:39:17 +08:00
majingjing123
906d819454 feat(audio): Use esp_audio_codec and esp_audio_effects to replace 78opus (#1632) 2026-01-07 18:45:34 +08:00
laride
be88719932 feat: Add ESP-SensairShuttle (#1620)
* feat: Add ESP-SensairShuttle

* fix: fix board name
2026-01-02 12:19:46 +08:00
Wang is proud
213117ded2 BLUFI network configuration supports encryption. (#1603) 2025-12-29 15:04:29 +08:00
Wang is proud
76ff1cf0dc Fixed the issue where voice wake-up causes panic when DEBUG is enabled. (#1606) 2025-12-29 15:03:02 +08:00
zczc365
5d44633687 feat: 小智云聊增加两个语音指令 (#1596) 2025-12-26 03:01:18 +08:00
Kevincoooool
5113a5f4bb Add power management for ESP32S3-Korvo2-V3 board (#1591)
Introduces PowerManager and integrates it into the ESP32S3-Korvo2-V3 board class to monitor battery level, charging status, and manage power save mode. Adds power_manager.h with battery ADC reading, calibration, and event callbacks. Updates board initialization to support power management and power save timer functionality.
2025-12-22 20:33:02 +08:00
pfangzhi
f501a5f440 add boards waveshare-c6-touch-lcd-1.83 (#1575)
* Update CMakeLists.txt 

WAVESHARE_C6_TOUCH_LCD_1_83

* Update Kconfig.projbuild

WAVESHARE_C6_TOUCH_LCD_1_83

* Create README.md

waveshare-c6-touch-lcd-1.83

* Add files via upload
2025-12-22 14:11:18 +08:00
MOV
e9649cfc58 feat: add support for Movecall Moji2 board (ESP32-C5) (#1589)
* add: Moji 2 has built-in ESP32-C5 dual band Wi-Fi

* feat: PowerSaveTimer 160 >> 240
2025-12-21 18:57:53 +08:00
Denis Ryabchikov
ee5587019b Fix issue "Missing spaces" (#1582)
Co-authored-by: xuxin <xuxin@espressif.com>
2025-12-18 20:01:00 +08:00
Denis Ryabchikov
ebdd58748a echoear: improved quality of audio assets for Russian language (#1584)
Co-authored-by: Рябчиков Денис <DRyabchikov_od@inno.tech>
2025-12-18 07:02:55 +08:00
Wang is proud
99c32d9331 Fixed the bug where BLUFI would output error logs even when the network connection was normal. (#1567) 2025-12-17 06:18:41 +08:00
Kevincoooool
cccaf71c3e Enable camera configs and update button/camera logic (#1561)
Added camera-related sdkconfig options to esp32s3-korvo2-v3 and kevin-sp-v4-dev config.json files. Updated esp32s3_korvo2_v3_board.cc to implement button actions for WiFi config and chat state toggling. Refactored kevin-sp-v4_board.cc to unify I2C bus usage for codec and camera, and adjusted SCCB initialization logic.
2025-12-15 20:16:56 +08:00
Xiaoxia
564018c762 Enhance audio feedback mechanism by introducing a flag to play a popup (#1560)
* Enhance audio feedback mechanism by introducing a flag to play a popup sound after transitioning to listening mode. Update Schedule method to accept rvalue references for callbacks. Bump esp-ml307 component version to 3.5.3. Adjust signal strength thresholds in Ml307Board for better accuracy.

* Update esp-wifi-connect component version to 3.0.2 in idf_component.yml

* Adjust Wi-Fi signal strength thresholds in WifiBoard for improved accuracy in network state icon representation.
2025-12-15 12:54:17 +08:00
神奇bug在哪里
0ccdc082b5 feat: BluFi Wi-Fi configuration (#1321)
Signed-off-by: WhereAreBugs <wherearebugs@icloud.com>
2025-12-13 01:43:32 +08:00
laride
4b582f8074 feat: support USB Camera (#1519) 2025-12-11 07:10:12 +08:00
Tomato Me
1f0d2e993b fix incorrect json configuartion (#1541) 2025-12-10 21:07:56 +08:00
Xiaoxia
b7db68457c v2.1.0: Upgrade esp-wifi-connect to 3.0; New device state machine (#1528)
* Upgrade component version

* update fonts component version

* Handle OTA error code

* Update project version to 2.1.0 and add device state machine implementation

- Upgrade  esp-wifi-connect to 3.0.0, allowing reconfiguring wifi without rebooting
- Introduce device state machine with state change notification in new files
- Remove obsolete device state event files
- Update application logic to utilize new state machine
- Minor adjustments in various board implementations for state handling

* fix compile errors

* Refactor power saving mode implementation to use PowerSaveLevel enumeration

- Updated Application class to replace SetPowerSaveMode with SetPowerSaveLevel, allowing for LOW_POWER and PERFORMANCE settings.
- Modified various board implementations to align with the new power save level structure.
- Ensured consistent handling of power save levels across different board files, enhancing code maintainability and clarity.

* Refactor power save level checks across multiple board implementations

- Updated the condition for power save level checks in various board files to ensure that the power save timer only wakes up when the level is not set to LOW_POWER.
- Improved consistency in handling power save levels, enhancing code clarity and maintainability.

* Refactor EnterWifiConfigMode calls in board implementations

- Updated calls to EnterWifiConfigMode to use the appropriate instance reference (self or board) across multiple board files.
- Improved code consistency and clarity in handling device state during WiFi configuration mode entry.

* Add cellular modem event handling and improve network status updates

- Introduced new network events for cellular modem operations, including detecting, registration errors, and timeouts.
- Enhanced the Application class to handle different network states and update the display status accordingly.
- Refactored Ml307Board to implement a callback mechanism for network events, improving modularity and responsiveness.
- Updated dual_network_board and board headers to support new network event callbacks, ensuring consistent handling across board implementations.

* update esp-wifi-connect version

* Update WiFi configuration tool messages across multiple board implementations to clarify user actions
2025-12-09 09:24:56 +08:00
小鹏
11c79a7003 Refactor Otto Robot configuration and initialization (#1534)
* otto v1.4.0 MCP

1.使用MCP协议控制机器人
2.gif继承lcdDisplay,避免修改lcdDisplay

* otto v1.4.1 gif as components

gif as components

* electronBot v1.1.0 mcp

1.增加electronBot支持
2.mcp协议
3.gif 作为组件
4.display子类

* 规范代码

1.规范代码
2.修复切换主题死机bug

* fix(ota): 修复 ottoRobot和electronBot OTA 升级崩溃问题 bug

* 1.增加robot舵机初始位置校准
2.fix(mcp_sever) 超出范围异常捕获类型  bug

* refactor: Update Electron and Otto emoji display implementations

- Removed GIF selection from Kconfig for Electron and Otto boards.
- Updated Electron and Otto bot versions to 2.0.4 in their respective config files.
- Refactored emoji display classes to utilize EmojiCollection for managing emojis.
- Enhanced chat label setup and status display functionality in both classes.
- Cleaned up unused code and improved initialization logging for emoji displays.

* Rename OTTO_ICON_FONT.c to otto_icon_font.c

* Rename OTTO_ICON_FONT.c to otto_icon_font.c

* refactor: Update Otto emoji display configurations and functionalities

- Changed chat label text mode to circular scrolling for both Otto and Electron emoji displays.
- Bumped Otto robot version to 2.0.5 in the configuration file.
- Added new actions for Otto robot including Sit, WhirlwindLeg, Fitness, Greeting, Shy, RadioCalisthenics, MagicCircle, and Showcase.
- Enhanced servo sequence handling and added support for executing custom servo sequences.
- Improved logging and error handling for servo sequence execution.

* refactor: Update chat label long mode for Electron and Otto emoji displays

- Changed chat label text mode from wrap to circular scrolling for both Electron and Otto emoji displays.
- Improved consistency in chat label setup across both implementations.

* Update Otto robot README with new actions and parameters

* Update Otto controller parameters for oscillation settings

- Changed default oscillation period from 500ms to 300ms.
- Increased default steps from 5.0 to 8.0.
- Updated default amplitude from 20 degrees to 0 degrees.
- Enhanced documentation with new examples for oscillation modes and sequences.

* Fix default amplitude initialization in Otto controller to use a single zero instead of two digits.

* chore: update txp666/otto-emoji-gif-component version to 1.0.3 in idf_component.yml

* Refactor Otto controller
- Consolidated movement actions into a unified tool for the Otto robot, allowing for a single action command with various parameters.
- Removed individual movement tools (walk, turn, jump, etc.) and replaced them with a more flexible action system.

* Enhance Otto robot functionality by adding WebSocket control server and IP address retrieval feature. Updated config to support WebSocket, and revised README to include new control options and usage examples.

* Add camera support for Otto Robot board

- Introduced configuration option to enable the Otto Robot camera in Kconfig.
- Updated config.h to define camera-related GPIO pins and settings.
- Modified config.json to include camera configuration.
- Enhanced otto_robot.cc to initialize I2C and camera components when the camera is enabled.
- Adjusted power_manager.h to manage battery updates during camera operations.
- Removed unused SetupChatLabel method from OttoEmojiDisplay class.

* Refactor Otto Robot configuration and initialization

- Removed the camera configuration option from Kconfig and related code.
- Introduced a new HardwareConfig struct to encapsulate hardware pin definitions and settings.
- Updated config.h to define camera and non-camera configurations using the new struct.
- Refactored otto_controller.cc and otto_robot.cc to utilize the HardwareConfig struct for initialization.
- Enhanced camera detection and initialization logic based on hardware version.
- Improved audio codec initialization based on configuration settings.
2025-12-08 20:55:23 +08:00
小鹏
f9de29519b Add camera support for Otto Robot board (#1520)
* otto v1.4.0 MCP

1.使用MCP协议控制机器人
2.gif继承lcdDisplay,避免修改lcdDisplay

* otto v1.4.1 gif as components

gif as components

* electronBot v1.1.0 mcp

1.增加electronBot支持
2.mcp协议
3.gif 作为组件
4.display子类

* 规范代码

1.规范代码
2.修复切换主题死机bug

* fix(ota): 修复 ottoRobot和electronBot OTA 升级崩溃问题 bug

* 1.增加robot舵机初始位置校准
2.fix(mcp_sever) 超出范围异常捕获类型  bug

* refactor: Update Electron and Otto emoji display implementations

- Removed GIF selection from Kconfig for Electron and Otto boards.
- Updated Electron and Otto bot versions to 2.0.4 in their respective config files.
- Refactored emoji display classes to utilize EmojiCollection for managing emojis.
- Enhanced chat label setup and status display functionality in both classes.
- Cleaned up unused code and improved initialization logging for emoji displays.

* Rename OTTO_ICON_FONT.c to otto_icon_font.c

* Rename OTTO_ICON_FONT.c to otto_icon_font.c

* refactor: Update Otto emoji display configurations and functionalities

- Changed chat label text mode to circular scrolling for both Otto and Electron emoji displays.
- Bumped Otto robot version to 2.0.5 in the configuration file.
- Added new actions for Otto robot including Sit, WhirlwindLeg, Fitness, Greeting, Shy, RadioCalisthenics, MagicCircle, and Showcase.
- Enhanced servo sequence handling and added support for executing custom servo sequences.
- Improved logging and error handling for servo sequence execution.

* refactor: Update chat label long mode for Electron and Otto emoji displays

- Changed chat label text mode from wrap to circular scrolling for both Electron and Otto emoji displays.
- Improved consistency in chat label setup across both implementations.

* Update Otto robot README with new actions and parameters

* Update Otto controller parameters for oscillation settings

- Changed default oscillation period from 500ms to 300ms.
- Increased default steps from 5.0 to 8.0.
- Updated default amplitude from 20 degrees to 0 degrees.
- Enhanced documentation with new examples for oscillation modes and sequences.

* Fix default amplitude initialization in Otto controller to use a single zero instead of two digits.

* chore: update txp666/otto-emoji-gif-component version to 1.0.3 in idf_component.yml

* Refactor Otto controller
- Consolidated movement actions into a unified tool for the Otto robot, allowing for a single action command with various parameters.
- Removed individual movement tools (walk, turn, jump, etc.) and replaced them with a more flexible action system.

* Enhance Otto robot functionality by adding WebSocket control server and IP address retrieval feature. Updated config to support WebSocket, and revised README to include new control options and usage examples.

* Add camera support for Otto Robot board

- Introduced configuration option to enable the Otto Robot camera in Kconfig.
- Updated config.h to define camera-related GPIO pins and settings.
- Modified config.json to include camera configuration.
- Enhanced otto_robot.cc to initialize I2C and camera components when the camera is enabled.
- Adjusted power_manager.h to manage battery updates during camera operations.
- Removed unused SetupChatLabel method from OttoEmojiDisplay class.
2025-12-05 23:05:44 +08:00
NologoTech
d7c1aef77a 增加无名科技星智铝合金 wifi版本 的board支持 (#1352)
* 增加无名科技星智铝合金 wifi版本 的board支持

* 删除多余头文件

* 星智铝合金:触摸单击时间改为200ms

---------

Co-authored-by: Limh2017 <wenwen19115>
2025-12-01 19:38:14 +08:00
Piat Jonathan
33c2fe90a8 Support for DIY AI Watch HU-087 kit (#1445)
* Added new board for hu-087 smart AI watch

* Added a README with link to prouct on Aliexpress

* Trying to control chatbot mode instead of listening state

* Removed the esp32-s3 prefix for the board

* Changed board name to match folder naming

* Fixed formating and long URL

* Fixed typo in README.md
2025-12-01 19:30:40 +08:00
Denis Ryabchikov
e8f68a331f Fix: Build fails for esp32s3 Echoear (#1505) (#1507)
Co-authored-by: Рябчиков Денис <DRyabchikov_od@inno.tech>
2025-11-30 12:44:23 +08:00
tojoevan
2d15bef298 Add GetBatteryLevel for Waveshare ESP32-S3-ePaper-1.54 board (#1506) 2025-11-30 11:44:07 +08:00
virgil
28db4bd60a Fix sensecap watcher inference (#1501)
* feat: add keepalive check for Himax client and handle restart

* fix: adjust layout and positioning for top and bottom bars

* fix: fix other param restoring default ​​when modifying one param .
2025-11-30 10:48:14 +08:00
Y1hsiaochunnn
01a12b325f Fix the touch initialization issue. (#1497) 2025-11-28 06:44:59 +08:00
Y1hsiaochunnn
c87b1eabf4 Add Camera support for Waveshare's ESP32-P4 series development board (#1459)
* Add Camera support to Waveshare's ESP32-P4 series development board

* Fix i2c handle error
2025-11-22 15:37:22 +08:00
laride
908c9d5708 feat: add esp-spot c5 (#1462)
* feat: add esp-spot c5

* fix: fix table custom filename
2025-11-20 15:52:49 +08:00
344 changed files with 19704 additions and 4581 deletions

126
.clang-format Normal file
View File

@@ -0,0 +1,126 @@
---
Language: Cpp
BasedOnStyle: Google
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Left
AlignOperands: true
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterReturnType: ExceptShortType
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: false
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 100
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- cJSON_ArrayForEach
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^<esp_.*\.h>'
Priority: 1
- Regex: '^<driver/.*\.h>'
Priority: 1
- Regex: '^<.*\.h>'
Priority: 2
- Regex: '^<.*'
Priority: 3
- Regex: '.*'
Priority: 4
IncludeIsMainRegex: '([-_](test|unittest))?$'
IndentCaseLabels: true
IndentPPDirectives: None
IndentWidth: 4
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
PointerAlignment: Left
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- 'c++'
- 'C++'
CanonicalDelimiter: ''
BasedOnStyle: google
ReflowComments: true
SortIncludes: true
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Latest
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 4
UseTab: Never

View File

@@ -92,7 +92,7 @@ jobs:
include: ${{ fromJson(needs.prepare.outputs.variants) }}
runs-on: ubuntu-latest
container:
image: espressif/idf:release-v5.5
image: espressif/idf:v5.5.2
steps:
- name: Checkout
uses: actions/checkout@v4

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ tmp/
components/
managed_components/
build/
dist/
.vscode/
.devcontainer/
sdkconfig.old

View File

@@ -1,14 +1,13 @@
# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five lines of boilerplate have to be in your project's
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
set(PROJECT_VER "2.0.5")
# Add this line to disable the specific warning
add_compile_options(-Wno-missing-field-initializers)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(xiaozhi)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
set(PROJECT_VER "2.2.2")
project(xiaozhi)

View File

@@ -140,6 +140,7 @@ For server deployment on personal computers, refer to the following open-source
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python server
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java server
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang server
- [hackers365/xiaozhi-esp32-server-golang](https://github.com/hackers365/xiaozhi-esp32-server-golang) Golang server
Other client projects using the XiaoZhi communication protocol:
@@ -159,7 +160,7 @@ This is an open-source ESP32 project, released under the MIT license, allowing a
We hope this project helps everyone understand AI hardware development and apply rapidly evolving large language models to real hardware devices.
If you have any ideas or suggestions, please feel free to raise Issues or join the QQ group: 1011329060
If you have any ideas or suggestions, please feel free to raise Issues or join our [Discord](https://discord.gg/bXqgAfRm) or QQ group: 994694848
## Star History

View File

@@ -140,6 +140,7 @@ Feishuドキュメントチュートリアルをご覧ください
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Pythonサーバー
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Javaサーバー
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golangサーバー
- [hackers365/xiaozhi-esp32-server-golang](https://github.com/hackers365/xiaozhi-esp32-server-golang) Golangサーバー
シャオジー通信プロトコルを利用した他のクライアントプロジェクト:
@@ -155,7 +156,7 @@ Feishuドキュメントチュートリアルをご覧ください
このプロジェクトを通じて、AIハードウェア開発を理解し、急速に進化する大規模言語モデルを実際のハードウェアデバイスに応用できるようになることを目指しています。
ご意見やご提案があれば、いつでもIssueを提出するか、QQグループ1011329060 にご参加ください。
ご意見やご提案があれば、いつでもIssueを提出するか、[Discord](https://discord.gg/bXqgAfRm) または QQグループ1011329060 にご参加ください。
## スター履歴

View File

@@ -140,6 +140,7 @@ v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python 服务器
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java 服务器
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang 服务器
- [hackers365/xiaozhi-esp32-server-golang](https://github.com/hackers365/xiaozhi-esp32-server-golang) Golang 服务器
使用小智通信协议的第三方客户端项目:
@@ -155,7 +156,7 @@ v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版
我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。
如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群1011329060
如果你有任何想法或建议,请随时提出 Issues 或加入 [Discord](https://discord.gg/bXqgAfRm) 或 QQ 群1011329060
## Star History

View File

@@ -0,0 +1 @@
.

37
docs/blufi.md Normal file
View File

@@ -0,0 +1,37 @@
# BluFi 配网(集成 esp-wifi-connect
本文档说明如何在小智固件中启用和使用 BluFiBLE WiFi 配网),并结合项目内置的 `esp-wifi-connect` 组件完成 WiFi 连接与存储。官方
BluFi
协议说明请参考 [Espressif 文档](https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/api-guides/ble/blufi.html)。
## 前置条件
- 需要支持 BLE 的芯片与固件配置。
-`idf.py menuconfig` 中启用 `WiFi Configuration Method -> Esp Blufi``CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING=y`
)。如果想使用 BluFi必须关闭同一菜单下的 Hotspot 选项,否则默认使用 Hotspot 配网模式。
- 保持默认的 NVS 与事件循环初始化(项目的 `app_main` 已处理)。
- CONFIG_BT_BLUEDROID_ENABLED、CONFIG_BT_NIMBLE_ENABLED这两个宏应二选一不能同时启用。
## 工作流程
1) 手机端通过 BluFi如官方 EspBlufi App 或自研客户端)连接设备,发送 WiFi SSID/密码手机端可以通过blufi协议获取设备端扫描到的WiFi列表。
2) 设备侧在 `ESP_BLUFI_EVENT_REQ_CONNECT_TO_AP` 中将凭据写入 `SsidManager`(存储到 NVS属于 `esp-wifi-connect` 组件)。
3) 随后启动 `WifiStation` 扫描并连接;状态通过 BluFi 返回。
4) 配网成功后设备会自动连接新 WiFi失败则返回失败状态。
## 使用步骤
1. 配置:在 menuconfig 开启 `Esp Blufi`。编译并烧录固件。
2. 触发配网:设备首次启动且没有已保存的 WiFi 时会自动进入配网。
3. 手机端操作:打开 EspBlufi App或其他 BluFi 客户端),搜索并连接设备,可以选择是否加密,按提示输入 WiFi SSID/密码并发送。
4. 观察结果:
- 成功BluFi 报告连接成功,设备自动连接 WiFi。
- 失败BluFi 返回失败状态,可重新发送或检查路由器。
## 注意事项
- BluFi 配网不支持与热点配网同时开启。如果热点配网已经启动,则默认使用热点配网。请在 menuconfig 中只保留一种配网方式。
- 若多次测试,建议清除或覆盖存储的 SSID`wifi` 命名空间),避免旧配置干扰。
- 如果使用自定义 BluFi 客户端,需遵循官方协议帧格式,参考上文官方文档链接。
- 官方文档中已提供EspBlufi APP下载地址
- 由于IDF5.5.2的blufi接口发生变化,5.5.2版本编译后蓝牙名称为"Xiaozhi-Blufi",5.5.1版本中蓝牙名称为"BLUFI_DEVICE"

91
docs/code_style.md Normal file
View File

@@ -0,0 +1,91 @@
# 代码风格指南
## 代码格式化工具
本项目使用 clang-format 工具来统一代码风格。我们已经在项目根目录下提供了 `.clang-format` 配置文件,该配置基于 Google C++ 风格指南,并做了一些自定义调整。
### 安装 clang-format
在使用之前,请确保你已经安装了 clang-format 工具:
- **Windows**
```powershell
winget install LLVM
# 或者使用 Chocolatey
choco install llvm
```
- **Linux**
```bash
sudo apt install clang-format # Ubuntu/Debian
sudo dnf install clang-tools-extra # Fedora
```
- **macOS**
```bash
brew install clang-format
```
### 使用方法
1. **格式化单个文件**
```bash
clang-format -i path/to/your/file.cpp
```
2. **格式化整个项目**
```bash
# 在项目根目录下执行
find main -iname *.h -o -iname *.cc | xargs clang-format -i
```
3. **在提交代码前检查格式**
```bash
# 检查文件格式是否符合规范(不修改文件)
clang-format --dry-run -Werror path/to/your/file.cpp
```
### IDE 集成
- **Visual Studio Code**
1. 安装 C/C++ 扩展
2. 在设置中启用 `C_Cpp.formatting` 为 `clang-format`
3. 可以设置保存时自动格式化:`editor.formatOnSave: true`
- **CLion**
1. 在设置中选择 `Editor > Code Style > C/C++`
2. 将 `Formatter` 设置为 `clang-format`
3. 选择使用项目中的 `.clang-format` 配置文件
### 主要格式规则
- 缩进使用 4 个空格
- 行宽限制为 100 字符
- 大括号采用 Attach 风格(与控制语句在同一行)
- 指针和引用符号靠左对齐
- 自动排序头文件包含
- 类访问修饰符缩进为 -4 空格
### 注意事项
1. 提交代码前请确保代码已经过格式化
2. 不要手动调整已格式化的代码对齐
3. 如果某段代码不希望被格式化,可以使用以下注释包围:
```cpp
// clang-format off
// 你的代码
// clang-format on
```
### 常见问题
1. **格式化失败**
- 检查 clang-format 版本是否过低
- 确认文件编码为 UTF-8
- 验证 .clang-format 文件语法是否正确
2. **与期望格式不符**
- 检查是否使用了项目根目录下的 .clang-format 配置
- 确认没有其他位置的 .clang-format 文件被优先使用
如有任何问题或建议,欢迎提出 issue 或 pull request。

View File

@@ -197,8 +197,9 @@ private:
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});

View File

@@ -33,7 +33,7 @@ set(SOURCES "audio/audio_codec.cc"
"application.cc"
"ota.cc"
"settings.cc"
"device_state_event.cc"
"device_state_machine.cc"
"assets.cc"
"main.cc"
)
@@ -41,10 +41,28 @@ set(SOURCES "audio/audio_codec.cc"
set(INCLUDE_DIRS "." "display" "display/lvgl_display" "display/lvgl_display/jpg" "audio" "protocols")
# Add board common files
file(GLOB BOARD_COMMON_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/common/*.cc)
list(APPEND SOURCES ${BOARD_COMMON_SOURCES})
list(APPEND SOURCES
"boards/common/board.cc"
"boards/common/wifi_board.cc"
"boards/common/ml307_board.cc"
"boards/common/nt26_board.cc"
"boards/common/dual_network_board.cc"
"boards/common/adc_battery_monitor.cc"
"boards/common/afsk_demod.cc"
"boards/common/axp2101.cc"
"boards/common/backlight.cc"
"boards/common/button.cc"
"boards/common/i2c_device.cc"
"boards/common/knob.cc"
"boards/common/power_save_timer.cc"
"boards/common/press_to_talk_mcp_tool.cc"
"boards/common/sleep_timer.cc"
"boards/common/sy6970.cc"
"boards/common/system_reset.cc"
)
list(APPEND INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/boards/common)
idf_build_get_property(build_components BUILD_COMPONENTS)
# Function to find component dynamically by pattern
function(find_component_by_pattern PATTERN COMPONENT_VAR PATH_VAR)
@@ -62,6 +80,8 @@ endfunction()
set(BUILTIN_TEXT_FONT font_puhui_14_1)
set(BUILTIN_ICON_FONT font_awesome_14_1)
set(EMOTE_RESOLUTION "320_240")
# Add board files according to BOARD_TYPE
# Set default assets if the board uses partition table V2
if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI)
@@ -72,6 +92,10 @@ elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307)
set(BOARD_TYPE "bread-compact-ml307")
set(BUILTIN_TEXT_FONT font_puhui_basic_14_1)
set(BUILTIN_ICON_FONT font_awesome_14_1)
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_NT26)
set(BOARD_TYPE "bread-compact-nt26")
set(BUILTIN_TEXT_FONT font_puhui_basic_14_1)
set(BUILTIN_ICON_FONT font_awesome_14_1)
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32)
set(BOARD_TYPE "bread-compact-esp32")
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32_LCD)
@@ -80,26 +104,28 @@ elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32_LCD)
set(BUILTIN_ICON_FONT font_awesome_14_1)
elseif(CONFIG_BOARD_TYPE_DF_K10)
set(BOARD_TYPE "df-k10")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_DF_S3_AI_CAM)
set(BOARD_TYPE "df-s3-ai-cam")
elseif(CONFIG_BOARD_TYPE_ESP_BOX_3)
set(BOARD_TYPE "esp-box-3")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
set(EMOTE_RESOLUTION "320_240")
elseif(CONFIG_BOARD_TYPE_ESP_BOX)
set(BOARD_TYPE "esp-box")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
set(EMOTE_RESOLUTION "320_240")
elseif(CONFIG_BOARD_TYPE_ESP_BOX_LITE)
set(BOARD_TYPE "esp-box-lite")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_2)
set(BOARD_TYPE "kevin-box-2")
set(BUILTIN_TEXT_FONT font_puhui_basic_14_1)
@@ -108,14 +134,14 @@ elseif(CONFIG_BOARD_TYPE_KEVIN_C3)
set(BOARD_TYPE "kevin-c3")
elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V3_DEV)
set(BOARD_TYPE "kevin-sp-v3-dev")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V4_DEV)
set(BOARD_TYPE "kevin-sp-v4-dev")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_KEVIN_YUYING_313LCD)
set(BOARD_TYPE "kevin-yuying-313lcd")
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
@@ -123,9 +149,9 @@ elseif(CONFIG_BOARD_TYPE_KEVIN_YUYING_313LCD)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV_S3)
set(BOARD_TYPE "lichuang-dev")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV_C3)
set(BOARD_TYPE "lichuang-c3-dev")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
@@ -175,6 +201,11 @@ elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_S3R_CAM_M12_ECHO_BASE)
set(BOARD_TYPE "atoms3r-cam-m12-echo-base")
elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_ECHOS3R)
set(BOARD_TYPE "atom-echos3r")
elseif(CONFIG_BOARD_TYPE_M5STACK_CARDPUTER_ADV)
set(BOARD_TYPE "m5stack-cardputer-adv")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_32)
elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_MATRIX_ECHO_BASE)
set(BOARD_TYPE "atommatrix-echo-base")
elseif(CONFIG_BOARD_TYPE_XMINI_C3_V3)
@@ -194,13 +225,20 @@ elseif(CONFIG_BOARD_TYPE_ESP_KORVO2_V3)
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_ESP_KORVO2_V3_RNDIS)
set(BOARD_TYPE "esp32s3-korvo2-v3-rndis")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_ESP_SPARKBOT)
set(BOARD_TYPE "esp-sparkbot")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_ESP_SPOT_S3)
set(BOARD_TYPE "esp-spot-s3")
set(BOARD_TYPE "esp-spot")
elseif(CONFIG_BOARD_TYPE_ESP_SPOT_C5)
set(BOARD_TYPE "esp-spot")
elseif(CONFIG_BOARD_TYPE_ESP_HI)
set(BOARD_TYPE "esp-hi")
# Set ESP_HI emoji directory for DEFAULT_ASSETS_EXTRA_FILES
@@ -210,6 +248,13 @@ elseif(CONFIG_BOARD_TYPE_ECHOEAR)
set(BUILTIN_TEXT_FONT font_puhui_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(EMOTE_RESOLUTION "360_360")
# set(EMOTE_EXTERNAL_PATH "${CMAKE_CURRENT_SOURCE_DIR}/boards/echoear/assets")
elseif(CONFIG_BOARD_TYPE_ESP_SENSAIRSHUTTLE)
set(BOARD_TYPE "esp-sensairshuttle")
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
set(BUILTIN_ICON_FONT font_awesome_16_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_32)
elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_AUDIO_BOARD)
set(BOARD_TYPE "waveshare-s3-audio-board")
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
@@ -220,6 +265,11 @@ elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_1_8)
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
set(BUILTIN_ICON_FONT font_awesome_30_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_1_8)
set(BOARD_TYPE "waveshare-c6-touch-amoled-1.8")
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
set(BUILTIN_ICON_FONT font_awesome_30_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_2_06)
set(BOARD_TYPE "waveshare-s3-touch-amoled-2.06")
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
@@ -235,6 +285,11 @@ elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_4B)
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
set(BUILTIN_ICON_FONT font_awesome_30_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_4_3C)
set(BOARD_TYPE "waveshare-s3-touch-lcd-4.3c")
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
set(BUILTIN_ICON_FONT font_awesome_30_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_1_75)
set(BOARD_TYPE "waveshare-s3-touch-amoled-1.75")
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
@@ -274,6 +329,10 @@ elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_ePaper_1_54)
set(BOARD_TYPE "waveshare-s3-epaper-1.54")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_RLCD_4_2)
set(BOARD_TYPE "waveshare-s3-rlcd-4.2")
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
set(BUILTIN_ICON_FONT font_awesome_30_4)
elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_3_49)
set(BOARD_TYPE "waveshare-s3-touch-lcd-3.49")
set(LVGL_TEXT_FONT font_puhui_basic_30_4)
@@ -284,6 +343,11 @@ elseif(CONFIG_BOARD_TYPE_WAVESHARE_C6_LCD_1_69)
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_WAVESHARE_C6_TOUCH_LCD_1_83)
set(BOARD_TYPE "waveshare-c6-touch-lcd-1.83")
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
set(BUILTIN_ICON_FONT font_awesome_16_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_1_43)
set(BOARD_TYPE "waveshare-c6-touch-amoled-1.43")
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
@@ -365,6 +429,11 @@ elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI_ESP32S3)
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI2_ESP32C5)
set(BOARD_TYPE "movecall-moji2-esp32c5")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_MOVECALL_CUICAN_ESP32S3)
set(BOARD_TYPE "movecall-cuican-esp32s3")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
@@ -372,29 +441,29 @@ elseif(CONFIG_BOARD_TYPE_MOVECALL_CUICAN_ESP32S3)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3)
set(BOARD_TYPE "atk-dnesp32s3")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX)
set(BOARD_TYPE "atk-dnesp32s3-box")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX0)
set(BOARD_TYPE "atk-dnesp32s3-box0")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX2_WIFI)
set(BOARD_TYPE "atk-dnesp32s3-box2-wifi")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX2_4G)
set(BOARD_TYPE "atk-dnesp32s3-box2-4g")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3M_WIFI)
set(BOARD_TYPE "atk-dnesp32s3m-wifi")
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
@@ -435,19 +504,24 @@ elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_0_96OLED_ML307)
set(BUILTIN_ICON_FONT font_awesome_14_1)
elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_1_54TFT_WIFI)
set(BOARD_TYPE "xingzhi-cube-1.54tft-wifi")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_1_54TFT_ML307)
set(BOARD_TYPE "xingzhi-cube-1.54tft-ml307")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_XINGZHI_METAL_1_54_WIFI)
set(BOARD_TYPE "xingzhi-metal-1.54-wifi")
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_SEEED_STUDIO_SENSECAP_WATCHER)
set(BOARD_TYPE "sensecap-watcher")
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
set(BUILTIN_TEXT_FONT font_noto_basic_30_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
elseif(CONFIG_BOARD_TYPE_DOIT_S3_AIBOX)
set(BOARD_TYPE "doit-s3-aibox")
elseif(CONFIG_BOARD_TYPE_MIXGO_NOVA)
@@ -493,6 +567,16 @@ elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_1_54TFT_ML307)
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_CAM)
set(BOARD_TYPE "zhengchen-cam")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_CAM_ML307)
set(BOARD_TYPE "zhengchen-cam-ml307")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_SPOTPEAR_ESP32_S3_1_54_MUMA)
set(BOARD_TYPE "sp-esp32-s3-1.54-muma")
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
@@ -507,10 +591,12 @@ elseif(CONFIG_BOARD_TYPE_OTTO_ROBOT)
set(BOARD_TYPE "otto-robot")
set(BUILTIN_TEXT_FONT font_puhui_16_4)
set(BUILTIN_ICON_FONT font_awesome_16_4)
set(DEFAULT_EMOJI_COLLECTION otto-gif)
elseif(CONFIG_BOARD_TYPE_ELECTRON_BOT)
set(BOARD_TYPE "electron-bot")
set(BUILTIN_TEXT_FONT font_puhui_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION otto-gif)
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_CAM)
set(BOARD_TYPE "bread-compact-wifi-s3cam")
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
@@ -551,6 +637,10 @@ elseif(CONFIG_BOARD_TYPE_AIPI_LITE)
set(BUILTIN_TEXT_FONT font_puhui_basic_14_1)
set(BUILTIN_ICON_FONT font_awesome_14_1)
set(DEFAULT_EMOJI_COLLECTION twemoji_32)
elseif(CONFIG_BOARD_TYPE_HU_087)
set(BOARD_TYPE "hu-087")
set(BUILTIN_TEXT_FONT font_puhui_basic_14_1)
set(BUILTIN_ICON_FONT font_awesome_14_1)
endif()
file(GLOB BOARD_SOURCES
@@ -572,6 +662,10 @@ else()
list(APPEND SOURCES "audio/wake_words/esp_wake_word.cc")
endif()
# Auto Select Additional Sources
if (CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING)
list(APPEND SOURCES "boards/common/blufi.cpp")
endif ()
# Select language directory according to Kconfig
if(CONFIG_LANGUAGE_ZH_CN)
set(LANG_DIR "zh-CN")
@@ -687,16 +781,47 @@ if(CONFIG_IDF_TARGET_ESP32)
"audio/codecs/es8388_audio_codec.cc"
"audio/codecs/es8389_audio_codec.cc"
"led/gpio_led.cc"
"${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_camera.cc"
"display/lvgl_display/jpg/image_to_jpeg.cpp"
"display/lvgl_display/jpg/jpeg_to_image.c"
"boards/common/nt26_board.cc"
"boards/common/ml307_board.cc"
"boards/common/dual_network_board.cc"
)
endif()
# Include EspVideo if target is ESP32S3 or ESP32P4
if(CONFIG_IDF_TARGET_ESP32S3 OR CONFIG_IDF_TARGET_ESP32P4)
list(APPEND SOURCES "boards/common/esp_video.cc"
"boards/common/rndis_board.cc"
)
endif()
# Include Esp32Camera if target is ESP32S3
if(CONFIG_IDF_TARGET_ESP32S3)
list(APPEND SOURCES "boards/common/esp32_camera.cc")
endif()
idf_component_register(SRCS ${SOURCES}
EMBED_FILES ${LANG_SOUNDS} ${COMMON_SOUNDS}
INCLUDE_DIRS ${INCLUDE_DIRS}
WHOLE_ARCHIVE
PRIV_REQUIRES
esp_pm
esp_psram
esp_netif
esp_driver_gpio
esp_driver_uart
esp_driver_spi
esp_driver_i2c
esp_driver_i2s
esp_driver_jpeg
esp_driver_ppa
esp_app_format
app_update
spi_flash
console
efuse
bt
)
# Use target_compile_definitions to define BOARD_TYPE, BOARD_NAME
@@ -898,6 +1023,14 @@ if ("${size}" AND "${offset}")
get_assets_local_file("${CONFIG_CUSTOM_ASSETS_FILE}" ASSETS_LOCAL_FILE)
esptool_py_flash_to_partition(flash "assets" "${ASSETS_LOCAL_FILE}")
message(STATUS "Custom assets flash configured: ${ASSETS_LOCAL_FILE} -> assets partition")
elseif(CONFIG_FLASH_EXPRESSION_ASSETS)
set(ASSETS_NAME "expression_assets")
set(ASSETS_PARTITION "assets")
set(ASSETS_FILE "${CMAKE_BINARY_DIR}/${ASSETS_NAME}.bin")
build_speaker_assets_bin("${ASSETS_PARTITION}" ${EMOTE_RESOLUTION} ${ASSETS_FILE} ${CONFIG_MMAP_FILE_NAME_LENGTH})
message(STATUS "Generated emote assets: ${ASSETS_FILE} -> ${ASSETS_PARTITION} partition")
esptool_py_flash_to_partition(flash "${ASSETS_PARTITION}" "${ASSETS_FILE}")
elseif(CONFIG_FLASH_NONE_ASSETS)
message(STATUS "Assets flashing disabled (FLASH_NONE_ASSETS)")
endif()

View File

@@ -8,7 +8,8 @@ config OTA_URL
choice
prompt "Flash Assets"
default FLASH_DEFAULT_ASSETS
default FLASH_DEFAULT_ASSETS if !USE_EMOTE_MESSAGE_STYLE
default FLASH_EXPRESSION_ASSETS if USE_EMOTE_MESSAGE_STYLE
help
Select the assets to flash.
@@ -16,8 +17,12 @@ choice
bool "Do not flash assets"
config FLASH_DEFAULT_ASSETS
bool "Flash Default Assets"
depends on !USE_EMOTE_MESSAGE_STYLE
config FLASH_CUSTOM_ASSETS
bool "Flash Custom Assets"
config FLASH_EXPRESSION_ASSETS
bool "Flash Emote Assets"
depends on USE_EMOTE_MESSAGE_STYLE
endchoice
config CUSTOM_ASSETS_FILE
@@ -129,6 +134,9 @@ choice BOARD_TYPE
config BOARD_TYPE_BREAD_COMPACT_ML307
bool "Bread Compact ML307/EC801E (面包板 4G)"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_BREAD_COMPACT_NT26
bool "Bread Compact NT26 (面包板 4G)"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_BREAD_COMPACT_ESP32
bool "Bread Compact ESP32 DevKit (面包板)"
depends on IDF_TARGET_ESP32
@@ -147,12 +155,21 @@ choice BOARD_TYPE
config BOARD_TYPE_ESP_KORVO2_V3
bool "Espressif Korvo2 V3"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ESP_KORVO2_V3_RNDIS
bool "Espressif Korvo2 V3 RNDIS"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ESP_SPARKBOT
bool "Espressif SparkBot"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ESP_SENSAIRSHUTTLE
bool "Espressif ESP-SensairShuttle"
depends on IDF_TARGET_ESP32C5
config BOARD_TYPE_ESP_SPOT_S3
bool "Espressif Spot-S3"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ESP_SPOT_C5
bool "Espressif Spot-C5"
depends on IDF_TARGET_ESP32C5
config BOARD_TYPE_ESP_HI
bool "Espressif ESP-HI"
depends on IDF_TARGET_ESP32C3
@@ -234,6 +251,9 @@ choice BOARD_TYPE
config BOARD_TYPE_M5STACK_ATOM_ECHOS3R
bool "M5Stack AtomEchoS3R"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_M5STACK_CARDPUTER_ADV
bool "M5Stack Cardputer Adv"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_M5STACK_ATOM_MATRIX_ECHO_BASE
bool "M5Stack AtomMatrix + Echo Base"
depends on IDF_TARGET_ESP32
@@ -258,6 +278,9 @@ choice BOARD_TYPE
config BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_4B
bool "Waveshare ESP32-S3-Touch-LCD-4B"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_4_3C
bool "Waveshare ESP32-S3-Touch-LCD-4.3C"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_1_85C
bool "Waveshare ESP32-S3-Touch-LCD-1.85C"
depends on IDF_TARGET_ESP32S3
@@ -270,12 +293,18 @@ choice BOARD_TYPE
config BOARD_TYPE_WAVESHARE_C6_LCD_1_69
bool "Waveshare ESP32-C6-LCD-1.69"
depends on IDF_TARGET_ESP32C6
config BOARD_TYPE_WAVESHARE_C6_TOUCH_LCD_1_83
bool "Waveshare ESP32-C6-Touch-LCD-1.83"
depends on IDF_TARGET_ESP32C6
config BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_1_43
bool "Waveshare ESP32-C6-Touch-AMOLOED-1.43"
depends on IDF_TARGET_ESP32C6
config BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_1_32
bool "Waveshare ESP32-C6-Touch-AMOLOED-1.32"
depends on IDF_TARGET_ESP32C6
config BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_1_8
bool "Waveshare ESP32-C6-Touch-AMOLED-1.8"
depends on IDF_TARGET_ESP32C6
config BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_1_32
bool "Waveshare ESP32-S3-Touch-AMOLOED-1.32"
depends on IDF_TARGET_ESP32S3
@@ -288,6 +317,9 @@ choice BOARD_TYPE
config BOARD_TYPE_WAVESHARE_S3_ePaper_1_54
bool "Waveshare ESP32-S3-ePaper-1.54"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_WAVESHARE_S3_RLCD_4_2
bool "Waveshare ESP32-S3-RLCD-4.2"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_3_5B
bool "Waveshare ESP32-S3-Touch-LCD-3.5B"
depends on IDF_TARGET_ESP32S3
@@ -327,6 +359,9 @@ choice BOARD_TYPE
config BOARD_TYPE_MOVECALL_MOJI_ESP32S3
bool "Movecall Moji 小智AI衍生版"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_MOVECALL_MOJI2_ESP32C5
bool "Movecall Moji2.0 小智AI衍生版"
depends on IDF_TARGET_ESP32C5
config BOARD_TYPE_MOVECALL_CUICAN_ESP32S3
bool "Movecall CuiCan 璀璨·AI吊坠"
depends on IDF_TARGET_ESP32S3
@@ -375,6 +410,9 @@ choice BOARD_TYPE
config BOARD_TYPE_XINGZHI_CUBE_1_54TFT_ML307
bool "无名科技星智1.54(ML307)"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_XINGZHI_METAL_1_54_WIFI
bool "无名科技星智1.54 METAL(wifi)"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_SEEED_STUDIO_SENSECAP_WATCHER
bool "Seeed Studio SenseCAP Watcher"
depends on IDF_TARGET_ESP32S3
@@ -399,6 +437,12 @@ choice BOARD_TYPE
config BOARD_TYPE_ZHENGCHEN_1_54TFT_ML307
bool "征辰科技1.54(ML307)"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ZHENGCHEN_CAM
bool "征辰科技AI Camera"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ZHENGCHEN_CAM_ML307
bool "征辰科技AI Camera(ML307)"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_MINSI_K08_DUAL
bool "敏思科技K08(DUAL)"
depends on IDF_TARGET_ESP32S3
@@ -435,6 +479,9 @@ choice BOARD_TYPE
config BOARD_TYPE_AIPI_LITE
bool "AIPI-Lite"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_HU_087
bool "HU-087"
depends on IDF_TARGET_ESP32S3
endchoice
choice
@@ -468,7 +515,7 @@ choice ESP_S3_LCD_EV_Board_Version_TYPE
endchoice
choice DISPLAY_OLED_TYPE
depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307 || BOARD_TYPE_BREAD_COMPACT_ESP32
depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307 || BOARD_TYPE_BREAD_COMPACT_NT26 || BOARD_TYPE_BREAD_COMPACT_ESP32 || BOARD_TYPE_HU_087
prompt "OLED Type"
default OLED_SSD1306_128X32
help
@@ -530,7 +577,7 @@ choice DISPLAY_LCD_TYPE
endchoice
choice DISPLAY_ESP32S3_KORVO2_V3
depends on BOARD_TYPE_ESP_KORVO2_V3
depends on BOARD_TYPE_ESP_KORVO2_V3 || BOARD_TYPE_ESP_KORVO2_V3_RNDIS
prompt "ESP32S3_KORVO2_V3 LCD Type"
default ESP32S3_KORVO2_V3_LCD_ST7789
help
@@ -567,7 +614,9 @@ choice DISPLAY_STYLE
config USE_EMOTE_MESSAGE_STYLE
bool "Emote animation style"
depends on BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ECHOEAR || BOARD_TYPE_LICHUANG_DEV_S3
depends on BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_3 \
|| BOARD_TYPE_ECHOEAR || BOARD_TYPE_LICHUANG_DEV_S3 \
|| BOARD_TYPE_ESP_SENSAIRSHUTTLE
endchoice
choice WAKE_WORD_TYPE
@@ -631,6 +680,16 @@ config SEND_WAKE_WORD_DATA
help
Send wake word data to the server as the first message of the conversation and wait for response
config WAKE_WORD_DETECTION_IN_LISTENING
bool "Enable Wake Word Detection in Listening Mode"
default n
depends on USE_AFE_WAKE_WORD || USE_CUSTOM_WAKE_WORD
help
Enable wake word detection while in listening mode.
When enabled, the device can detect wake word during listening,
which allows interrupting the current conversation.
When disabled (default), wake word detection is turned off during listening.
config USE_AUDIO_PROCESSOR
bool "Enable Audio Noise Reduction"
default y
@@ -645,7 +704,8 @@ config USE_DEVICE_AEC
|| BOARD_TYPE_LICHUANG_DEV_S3 || BOARD_TYPE_ESP_KORVO2_V3 || BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_1_75 || BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_1_83\
|| BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_2_06 || BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_4B || BOARD_TYPE_WAVESHARE_P4_WIFI6_TOUCH_LCD_4B || BOARD_TYPE_WAVESHARE_P4_WIFI6_TOUCH_LCD_7B \
|| BOARD_TYPE_WAVESHARE_P4_WIFI6_TOUCH_LCD_XC || BOARD_TYPE_ESP_S3_LCD_EV_Board_2 || BOARD_TYPE_YUNLIAO_S3 \
|| BOARD_TYPE_ECHOEAR || BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_3_49)
|| BOARD_TYPE_ECHOEAR || BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_3_49 || BOARD_TYPE_WAVESHARE_S3_RLCD_4_2 || BOARD_TYPE_ZHENGCHEN_CAM || BOARD_TYPE_ZHENGCHEN_CAM_ML307 \
|| BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_4_3C)
help
To work properly, device-side AEC requires a clean output reference path from the speaker signal and physical acoustic isolation between the microphone and speaker.
@@ -662,6 +722,29 @@ config USE_AUDIO_DEBUGGER
help
Enable audio debugger, send audio data through UDP to the host machine
menu "WiFi Configuration Method"
help
WiFi Configuration Method Selection
config USE_HOTSPOT_WIFI_PROVISIONING
bool "Hotspot"
default y
help
Use WiFi Hotspot to transmit WiFi configuration data
config USE_ACOUSTIC_WIFI_PROVISIONING
bool "Acoustic"
help
Use audio signal to transmit WiFi configuration data
config USE_ESP_BLUFI_WIFI_PROVISIONING
bool "Esp Blufi"
help
Use esp blufi protocol to transmit WiFi configuration data
select BT_ENABLED
select BT_BLE_42_FEATURES_SUPPORTED
select BT_BLE_BLUFI_ENABLE
select MBEDTLS_DHM_C
endmenu
config AUDIO_DEBUG_UDP_SERVER
string "Audio Debug UDP Server Address"
default "192.168.2.100:8000"
@@ -669,12 +752,6 @@ config AUDIO_DEBUG_UDP_SERVER
help
UDP server address, format: IP:PORT, used to receive audio debugging data
config USE_ACOUSTIC_WIFI_PROVISIONING
bool "Enable Acoustic WiFi Provisioning"
default n
help
Enable acoustic WiFi provisioning, use audio signal to transmit WiFi configuration data
config RECEIVE_CUSTOM_MESSAGE
bool "Enable Custom Message Reception"
default n
@@ -685,7 +762,7 @@ menu "Camera Configuration"
depends on !IDF_TARGET_ESP32
comment "Warning: Please read the help text before modifying these settings."
config XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
bool "Allow JPEG Input"
default n

File diff suppressed because it is too large Load Diff

View File

@@ -14,16 +14,23 @@
#include "protocol.h"
#include "ota.h"
#include "audio_service.h"
#include "device_state_event.h"
#include "device_state.h"
#include "device_state_machine.h"
#define MAIN_EVENT_SCHEDULE (1 << 0)
#define MAIN_EVENT_SEND_AUDIO (1 << 1)
#define MAIN_EVENT_WAKE_WORD_DETECTED (1 << 2)
#define MAIN_EVENT_VAD_CHANGE (1 << 3)
#define MAIN_EVENT_ERROR (1 << 4)
#define MAIN_EVENT_CHECK_NEW_VERSION_DONE (1 << 5)
#define MAIN_EVENT_CLOCK_TICK (1 << 6)
// Main event bits
#define MAIN_EVENT_SCHEDULE (1 << 0)
#define MAIN_EVENT_SEND_AUDIO (1 << 1)
#define MAIN_EVENT_WAKE_WORD_DETECTED (1 << 2)
#define MAIN_EVENT_VAD_CHANGE (1 << 3)
#define MAIN_EVENT_ERROR (1 << 4)
#define MAIN_EVENT_ACTIVATION_DONE (1 << 5)
#define MAIN_EVENT_CLOCK_TICK (1 << 6)
#define MAIN_EVENT_NETWORK_CONNECTED (1 << 7)
#define MAIN_EVENT_NETWORK_DISCONNECTED (1 << 8)
#define MAIN_EVENT_TOGGLE_CHAT (1 << 9)
#define MAIN_EVENT_START_LISTENING (1 << 10)
#define MAIN_EVENT_STOP_LISTENING (1 << 11)
#define MAIN_EVENT_STATE_CHANGED (1 << 12)
enum AecMode {
@@ -38,31 +45,80 @@ public:
static Application instance;
return instance;
}
// 删除拷贝构造函数和赋值运算符
// Delete copy constructor and assignment operator
Application(const Application&) = delete;
Application& operator=(const Application&) = delete;
void Start();
void MainEventLoop();
DeviceState GetDeviceState() const { return device_state_; }
/**
* Initialize the application
* This sets up display, audio, network callbacks, etc.
* Network connection starts asynchronously.
*/
void Initialize();
/**
* Run the main event loop
* This function runs in the main task and never returns.
* It handles all events including network, state changes, and user interactions.
*/
void Run();
DeviceState GetDeviceState() const { return state_machine_.GetState(); }
bool IsVoiceDetected() const { return audio_service_.IsVoiceDetected(); }
void Schedule(std::function<void()> callback);
void SetDeviceState(DeviceState state);
/**
* Request state transition
* Returns true if transition was successful
*/
bool SetDeviceState(DeviceState state);
/**
* Schedule a callback to be executed in the main task
*/
void Schedule(std::function<void()>&& callback);
/**
* Alert with status, message, emotion and optional sound
*/
void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");
void DismissAlert();
void AbortSpeaking(AbortReason reason);
/**
* Toggle chat state (event-based, thread-safe)
* Sends MAIN_EVENT_TOGGLE_CHAT to be handled in Run()
*/
void ToggleChatState();
/**
* Start listening (event-based, thread-safe)
* Sends MAIN_EVENT_START_LISTENING to be handled in Run()
*/
void StartListening();
/**
* Stop listening (event-based, thread-safe)
* Sends MAIN_EVENT_STOP_LISTENING to be handled in Run()
*/
void StopListening();
void Reboot();
void WakeWordInvoke(const std::string& wake_word);
bool UpgradeFirmware(Ota& ota, const std::string& url = "");
bool UpgradeFirmware(const std::string& url, const std::string& version = "");
bool CanEnterSleepMode();
void SendMcpMessage(const std::string& payload);
void SetAecMode(AecMode mode);
AecMode GetAecMode() const { return aec_mode_; }
void PlaySound(const std::string_view& sound);
AudioService& GetAudioService() { return audio_service_; }
/**
* Reset protocol resources (thread-safe)
* Can be called from any task to release resources allocated after network connected
* This includes closing audio channel, resetting protocol and ota objects
*/
void ResetProtocol();
private:
Application();
@@ -73,23 +129,46 @@ private:
std::unique_ptr<Protocol> protocol_;
EventGroupHandle_t event_group_ = nullptr;
esp_timer_handle_t clock_timer_handle_ = nullptr;
volatile DeviceState device_state_ = kDeviceStateUnknown;
DeviceStateMachine state_machine_;
ListeningMode listening_mode_ = kListeningModeAutoStop;
AecMode aec_mode_ = kAecOff;
std::string last_error_message_;
AudioService audio_service_;
std::unique_ptr<Ota> ota_;
bool has_server_time_ = false;
bool aborted_ = false;
bool assets_version_checked_ = false;
bool play_popup_on_listening_ = false; // Flag to play popup sound after state changes to listening
int clock_ticks_ = 0;
TaskHandle_t check_new_version_task_handle_ = nullptr;
TaskHandle_t main_event_loop_task_handle_ = nullptr;
TaskHandle_t activation_task_handle_ = nullptr;
void OnWakeWordDetected();
void CheckNewVersion(Ota& ota);
// Event handlers
void HandleStateChangedEvent();
void HandleToggleChatEvent();
void HandleStartListeningEvent();
void HandleStopListeningEvent();
void HandleNetworkConnectedEvent();
void HandleNetworkDisconnectedEvent();
void HandleActivationDoneEvent();
void HandleWakeWordDetectedEvent();
void ContinueOpenAudioChannel(ListeningMode mode);
void ContinueWakeWordInvoke(const std::string& wake_word);
// Activation task (runs in background)
void ActivationTask();
// Helper methods
void CheckAssetsVersion();
void CheckNewVersion();
void InitializeProtocol();
void ShowActivationCode(const std::string& code, const std::string& message);
void SetListeningMode(ListeningMode mode);
ListeningMode GetDefaultListeningMode() const;
// State change handler called by state machine
void OnStateChanged(DeviceState old_state, DeviceState new_state);
};

View File

@@ -4,17 +4,20 @@
#include "application.h"
#include "lvgl_theme.h"
#include "emote_display.h"
#ifdef HAVE_LVGL
#include "expression_emote.h"
#if HAVE_LVGL
#include "display/lcd_display.h"
#include <spi_flash_mmap.h>
#endif
#include <esp_log.h>
#include <spi_flash_mmap.h>
#include <esp_timer.h>
#include <esp_heap_caps.h>
#include <cbin_font.h>
#define TAG "Assets"
#define PARTITION_LABEL "assets"
struct mmap_assets_table {
char asset_name[32]; /*!< Name of the asset */
@@ -24,19 +27,99 @@ struct mmap_assets_table {
uint16_t asset_height; /*!< Height of the asset */
};
Assets::Assets() {
#if HAVE_LVGL
strategy_ = std::make_unique<Assets::LvglStrategy>();
#else
strategy_ = std::make_unique<Assets::EmoteStrategy>();
#endif
// Initialize the partition
InitializePartition();
}
Assets::~Assets() {
if (mmap_handle_ != 0) {
esp_partition_munmap(mmap_handle_);
UnApplyPartition();
}
bool Assets::FindPartition(Assets* assets) {
assets->partition_ = esp_partition_find_first(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, PARTITION_LABEL);
if (assets->partition_ == nullptr) {
ESP_LOGI(TAG, "No assets partition found");
return false;
}
return true;
}
bool Assets::Apply() {
return strategy_ ? strategy_->Apply(this) : false;
}
bool Assets::InitializePartition() {
return strategy_ ? strategy_->InitializePartition(this) : false;
}
void Assets::UnApplyPartition() {
if (strategy_) {
strategy_->UnApplyPartition(this);
}
}
uint32_t Assets::CalculateChecksum(const char* data, uint32_t length) {
bool Assets::GetAssetData(const std::string& name, void*& ptr, size_t& size) {
return strategy_ ? strategy_->GetAssetData(this, name, ptr, size) : false;
}
bool Assets::LoadSrmodelsFromIndex(Assets* assets, cJSON* root) {
void* ptr = nullptr;
size_t size = 0;
bool need_delete_root = false;
// If root is not provided, parse index.json
if (root == nullptr) {
if (!assets->GetAssetData("index.json", ptr, size)) {
ESP_LOGE(TAG, "The index.json file is not found");
return false;
}
root = cJSON_ParseWithLength(static_cast<char*>(ptr), size);
if (root == nullptr) {
ESP_LOGE(TAG, "The index.json file is not valid");
return false;
}
need_delete_root = true;
}
cJSON* srmodels = cJSON_GetObjectItem(root, "srmodels");
if (cJSON_IsString(srmodels)) {
std::string srmodels_file = srmodels->valuestring;
if (assets->GetAssetData(srmodels_file, ptr, size)) {
if (assets->models_list_ != nullptr) {
esp_srmodel_deinit(assets->models_list_);
assets->models_list_ = nullptr;
}
assets->models_list_ = srmodel_load(static_cast<uint8_t*>(ptr));
if (assets->models_list_ != nullptr) {
auto& app = Application::GetInstance();
app.GetAudioService().SetModelsList(assets->models_list_);
if (need_delete_root) {
cJSON_Delete(root);
}
return true;
} else {
ESP_LOGE(TAG, "Failed to load srmodels.bin");
}
} else {
ESP_LOGE(TAG, "The srmodels file %s is not found", srmodels_file.c_str());
}
}
if (need_delete_root) {
cJSON_Delete(root);
}
return false;
}
#if HAVE_LVGL
uint32_t Assets::LvglStrategy::CalculateChecksum(const char* data, uint32_t length) {
uint32_t checksum = 0;
for (uint32_t i = 0; i < length; i++) {
checksum += data[i];
@@ -44,40 +127,37 @@ uint32_t Assets::CalculateChecksum(const char* data, uint32_t length) {
return checksum & 0xFFFF;
}
bool Assets::InitializePartition() {
partition_valid_ = false;
checksum_valid_ = false;
bool Assets::LvglStrategy::InitializePartition(Assets* assets) {
assets->partition_valid_ = false;
assets_.clear();
partition_ = esp_partition_find_first(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, "assets");
if (partition_ == nullptr) {
ESP_LOGI(TAG, "No assets partition found");
if (!Assets::FindPartition(assets)) {
return false;
}
int free_pages = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA);
uint32_t storage_size = free_pages * 64 * 1024;
ESP_LOGI(TAG, "The storage free size is %ld KB", storage_size / 1024);
ESP_LOGI(TAG, "The partition size is %ld KB", partition_->size / 1024);
if (storage_size < partition_->size) {
ESP_LOGE(TAG, "The free size %ld KB is less than assets partition required %ld KB", storage_size / 1024, partition_->size / 1024);
ESP_LOGI(TAG, "The partition size is %ld KB", assets->partition_->size / 1024);
if (storage_size < assets->partition_->size) {
ESP_LOGE(TAG, "The free size %ld KB is less than assets partition required %ld KB", storage_size / 1024, assets->partition_->size / 1024);
return false;
}
esp_err_t err = esp_partition_mmap(partition_, 0, partition_->size, ESP_PARTITION_MMAP_DATA, (const void**)&mmap_root_, &mmap_handle_);
esp_err_t err = esp_partition_mmap(assets->partition_, 0, assets->partition_->size, ESP_PARTITION_MMAP_DATA, (const void**)&mmap_root_, &mmap_handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to mmap assets partition: %s", esp_err_to_name(err));
return false;
}
partition_valid_ = true;
assets->partition_valid_ = true;
uint32_t stored_files = *(uint32_t*)(mmap_root_ + 0);
uint32_t stored_chksum = *(uint32_t*)(mmap_root_ + 4);
uint32_t stored_len = *(uint32_t*)(mmap_root_ + 8);
if (stored_len > partition_->size - 12) {
ESP_LOGD(TAG, "The stored_len (0x%lx) is greater than the partition size (0x%lx) - 12", stored_len, partition_->size);
if (stored_len > assets->partition_->size - 12) {
ESP_LOGD(TAG, "The stored_len (0x%lx) is greater than the partition size (0x%lx) - 12", stored_len, assets->partition_->size);
return false;
}
@@ -104,10 +184,37 @@ bool Assets::InitializePartition() {
return checksum_valid_;
}
bool Assets::Apply() {
void Assets::LvglStrategy::UnApplyPartition(Assets* assets) {
if (mmap_handle_ != 0) {
esp_partition_munmap(mmap_handle_);
mmap_handle_ = 0;
mmap_root_ = nullptr;
}
checksum_valid_ = false;
assets_.clear();
(void)assets; // Unused parameter
}
bool Assets::LvglStrategy::GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) {
auto asset = assets_.find(name);
if (asset == assets_.end()) {
return false;
}
auto data = (const char*)(mmap_root_ + asset->second.offset);
if (data[0] != 'Z' || data[1] != 'Z') {
ESP_LOGE(TAG, "The asset %s is not valid with magic %02x%02x", name.c_str(), data[0], data[1]);
return false;
}
ptr = static_cast<void*>(const_cast<char*>(data + 2));
size = asset->second.size;
return true;
}
bool Assets::LvglStrategy::Apply(Assets* assets) {
void* ptr = nullptr;
size_t size = 0;
if (!GetAssetData("index.json", ptr, size)) {
if (!assets->GetAssetData("index.json", ptr, size)) {
ESP_LOGE(TAG, "The index.json file is not found");
return false;
}
@@ -125,28 +232,9 @@ bool Assets::Apply() {
return false;
}
}
cJSON* srmodels = cJSON_GetObjectItem(root, "srmodels");
if (cJSON_IsString(srmodels)) {
std::string srmodels_file = srmodels->valuestring;
if (GetAssetData(srmodels_file, ptr, size)) {
if (models_list_ != nullptr) {
esp_srmodel_deinit(models_list_);
models_list_ = nullptr;
}
models_list_ = srmodel_load(static_cast<uint8_t*>(ptr));
if (models_list_ != nullptr) {
auto& app = Application::GetInstance();
app.GetAudioService().SetModelsList(models_list_);
} else {
ESP_LOGE(TAG, "Failed to load srmodels.bin");
}
} else {
ESP_LOGE(TAG, "The srmodels file %s is not found", srmodels_file.c_str());
}
}
#ifdef HAVE_LVGL
Assets::LoadSrmodelsFromIndex(assets, root);
auto& theme_manager = LvglThemeManager::GetInstance();
auto light_theme = theme_manager.GetTheme("light");
auto dark_theme = theme_manager.GetTheme("dark");
@@ -154,7 +242,7 @@ bool Assets::Apply() {
cJSON* font = cJSON_GetObjectItem(root, "text_font");
if (cJSON_IsString(font)) {
std::string fonts_text_file = font->valuestring;
if (GetAssetData(fonts_text_file, ptr, size)) {
if (assets->GetAssetData(fonts_text_file, ptr, size)) {
auto text_font = std::make_shared<LvglCBinFont>(ptr);
if (text_font->font() == nullptr) {
ESP_LOGE(TAG, "Failed to load fonts.bin");
@@ -182,7 +270,7 @@ bool Assets::Apply() {
cJSON* file = cJSON_GetObjectItem(emoji, "file");
cJSON* eaf = cJSON_GetObjectItem(emoji, "eaf");
if (cJSON_IsString(name) && cJSON_IsString(file) && (NULL== eaf)) {
if (!GetAssetData(file->valuestring, ptr, size)) {
if (!assets->GetAssetData(file->valuestring, ptr, size)) {
ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, file->valuestring);
continue;
}
@@ -213,7 +301,7 @@ bool Assets::Apply() {
light_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring));
}
if (cJSON_IsString(background_image)) {
if (!GetAssetData(background_image->valuestring, ptr, size)) {
if (!assets->GetAssetData(background_image->valuestring, ptr, size)) {
ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring);
return false;
}
@@ -234,7 +322,7 @@ bool Assets::Apply() {
dark_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring));
}
if (cJSON_IsString(background_image)) {
if (!GetAssetData(background_image->valuestring, ptr, size)) {
if (!assets->GetAssetData(background_image->valuestring, ptr, size)) {
ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring);
return false;
}
@@ -262,137 +350,84 @@ bool Assets::Apply() {
ESP_LOGI(TAG, "Set hide_subtitle to %s", hide ? "true" : "false");
}
}
#elif defined(CONFIG_USE_EMOTE_MESSAGE_STYLE)
auto &board = Board::GetInstance();
auto display = board.GetDisplay();
auto emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
cJSON* font = cJSON_GetObjectItem(root, "text_font");
if (cJSON_IsString(font)) {
std::string fonts_text_file = font->valuestring;
if (GetAssetData(fonts_text_file, ptr, size)) {
auto text_font = std::make_shared<LvglCBinFont>(ptr);
if (text_font->font() == nullptr) {
ESP_LOGE(TAG, "Failed to load fonts.bin");
return false;
}
if (emote_display) {
emote_display->AddTextFont(text_font);
}
} else {
ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str());
}
}
cJSON* emoji_collection = cJSON_GetObjectItem(root, "emoji_collection");
if (cJSON_IsArray(emoji_collection)) {
int emoji_count = cJSON_GetArraySize(emoji_collection);
if (emote_display) {
for (int i = 0; i < emoji_count; i++) {
cJSON* icon = cJSON_GetArrayItem(emoji_collection, i);
if (cJSON_IsObject(icon)) {
cJSON* name = cJSON_GetObjectItem(icon, "name");
cJSON* file = cJSON_GetObjectItem(icon, "file");
if (cJSON_IsString(name) && cJSON_IsString(file)) {
if (GetAssetData(file->valuestring, ptr, size)) {
cJSON* eaf = cJSON_GetObjectItem(icon, "eaf");
bool lack_value = false;
bool loop_value = false;
int fps_value = 0;
if (cJSON_IsObject(eaf)) {
cJSON* lack = cJSON_GetObjectItem(eaf, "lack");
cJSON* loop = cJSON_GetObjectItem(eaf, "loop");
cJSON* fps = cJSON_GetObjectItem(eaf, "fps");
lack_value = lack ? cJSON_IsTrue(lack) : false;
loop_value = loop ? cJSON_IsTrue(loop) : false;
fps_value = fps ? fps->valueint : 0;
emote_display->AddEmojiData(name->valuestring, ptr, size,
static_cast<uint8_t>(fps_value),
loop_value, lack_value);
}
} else {
ESP_LOGE(TAG, "Emoji \"%10s\" image file %s is not found", name->valuestring, file->valuestring);
}
}
}
}
}
}
cJSON* icon_collection = cJSON_GetObjectItem(root, "icon_collection");
if (cJSON_IsArray(icon_collection)) {
if (emote_display) {
int icon_count = cJSON_GetArraySize(icon_collection);
for (int i = 0; i < icon_count; i++) {
cJSON* icon = cJSON_GetArrayItem(icon_collection, i);
if (cJSON_IsObject(icon)) {
cJSON* name = cJSON_GetObjectItem(icon, "name");
cJSON* file = cJSON_GetObjectItem(icon, "file");
if (cJSON_IsString(name) && cJSON_IsString(file)) {
if (GetAssetData(file->valuestring, ptr, size)) {
emote_display->AddIconData(name->valuestring, ptr, size);
} else {
ESP_LOGE(TAG, "Icon \"%10s\" image file %s is not found", name->valuestring, file->valuestring);
}
}
}
}
}
}
cJSON* layout_json = cJSON_GetObjectItem(root, "layout");
if (cJSON_IsArray(layout_json)) {
int layout_count = cJSON_GetArraySize(layout_json);
for (int i = 0; i < layout_count; i++) {
cJSON* layout_item = cJSON_GetArrayItem(layout_json, i);
if (cJSON_IsObject(layout_item)) {
cJSON* name = cJSON_GetObjectItem(layout_item, "name");
cJSON* align = cJSON_GetObjectItem(layout_item, "align");
cJSON* x = cJSON_GetObjectItem(layout_item, "x");
cJSON* y = cJSON_GetObjectItem(layout_item, "y");
cJSON* width = cJSON_GetObjectItem(layout_item, "width");
cJSON* height = cJSON_GetObjectItem(layout_item, "height");
if (cJSON_IsString(name) && cJSON_IsString(align) && cJSON_IsNumber(x) && cJSON_IsNumber(y)) {
int width_val = cJSON_IsNumber(width) ? width->valueint : 0;
int height_val = cJSON_IsNumber(height) ? height->valueint : 0;
if (emote_display) {
emote_display->AddLayoutData(name->valuestring, align->valuestring,
x->valueint, y->valueint, width_val, height_val);
}
} else {
ESP_LOGW(TAG, "Invalid layout item %d: missing required fields", i);
}
}
}
}
#endif
cJSON_Delete(root);
return true;
}
#endif // HAVE_LVGL
bool Assets::EmoteStrategy::InitializePartition(Assets* assets) {
assets->partition_valid_ = false;
if (!Assets::FindPartition(assets)) {
return false;
}
esp_err_t ret = ESP_ERR_INVALID_STATE;
auto display = Board::GetInstance().GetDisplay();
auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
if (emote_display && emote_display->GetEmoteHandle() != nullptr) {
const emote_data_t data = {
.type = EMOTE_SOURCE_PARTITION,
.source = {
.partition_label = PARTITION_LABEL,
},
.flags = {
.mmap_enable = true, //must be true here!!!
},
};
ret = emote_mount_assets(emote_display->GetEmoteHandle(), &data);
} else {
ESP_LOGE(TAG, "Emote display is not initialized");
}
assets->partition_valid_ = ((ret == ESP_OK) ? true : false);
return assets->partition_valid_;
}
void Assets::EmoteStrategy::UnApplyPartition(Assets* assets) {
auto display = Board::GetInstance().GetDisplay();
auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
if (emote_display && emote_display->GetEmoteHandle() != nullptr) {
emote_unmount_assets(emote_display->GetEmoteHandle());
}
(void)assets; // Unused parameter
}
bool Assets::EmoteStrategy::GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) {
auto display = Board::GetInstance().GetDisplay();
auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
if (emote_display && emote_display->GetEmoteHandle() != nullptr) {
const uint8_t* data = nullptr;
size_t data_size = 0;
if (ESP_OK == emote_get_asset_data_by_name(emote_display->GetEmoteHandle(), name.c_str(), &data, &data_size)) {
ptr = const_cast<void*>(static_cast<const void*>(data));
size = data_size;
return true;
}
ESP_LOGE(TAG, "Failed to get asset data by name: %s", name.c_str());
return false;
}
(void)assets; // Unused parameter
return false;
}
bool Assets::EmoteStrategy::Apply(Assets* assets) {
Assets::LoadSrmodelsFromIndex(assets);
auto display = Board::GetInstance().GetDisplay();
auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
if (emote_display && emote_display->GetEmoteHandle() != nullptr) {
emote_load_assets(emote_display->GetEmoteHandle());
}
return true;
}
bool Assets::Download(std::string url, std::function<void(int progress, size_t speed)> progress_callback) {
ESP_LOGI(TAG, "Downloading new version of assets from %s", url.c_str());
// 取消当前资源分区的内存映射
if (mmap_handle_ != 0) {
esp_partition_munmap(mmap_handle_);
mmap_handle_ = 0;
mmap_root_ = nullptr;
}
checksum_valid_ = false;
assets_.clear();
UnApplyPartition();
// 下载新的资源文件
auto network = Board::GetInstance().GetNetwork();
@@ -430,16 +465,21 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
SECTOR_SIZE, content_length, sectors_to_erase, total_erase_size);
// 写入新的资源文件到分区一边erase一边写入
char buffer[512];
char* buffer = (char*)heap_caps_malloc(SECTOR_SIZE, MALLOC_CAP_INTERNAL);
if (buffer == nullptr) {
ESP_LOGE(TAG, "Failed to allocate buffer");
return false;
}
size_t total_written = 0;
size_t recent_written = 0;
size_t current_sector = 0;
auto last_calc_time = esp_timer_get_time();
while (true) {
int ret = http->Read(buffer, sizeof(buffer));
int ret = http->Read(buffer, SECTOR_SIZE);
if (ret < 0) {
ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret));
heap_caps_free(buffer);
return false;
}
@@ -459,6 +499,7 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
// 确保擦除范围不超过分区大小
if (sector_end > partition_->size) {
ESP_LOGE(TAG, "Sector end (%u) exceeds partition size (%lu)", sector_end, partition_->size);
heap_caps_free(buffer);
return false;
}
@@ -466,6 +507,7 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
esp_err_t err = esp_partition_erase_range(partition_, sector_start, SECTOR_SIZE);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to erase sector %u at offset %u: %s", current_sector, sector_start, esp_err_to_name(err));
heap_caps_free(buffer);
return false;
}
@@ -476,6 +518,7 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
esp_err_t err = esp_partition_write(partition_, total_written, buffer, ret);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write to assets partition at offset %u: %s", total_written, esp_err_to_name(err));
heap_caps_free(buffer);
return false;
}
@@ -497,6 +540,7 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
}
http->Close();
heap_caps_free(buffer);
if (total_written != content_length) {
ESP_LOGE(TAG, "Downloaded size (%u) does not match expected size (%u)", total_written, content_length);
@@ -514,19 +558,3 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
return true;
}
bool Assets::GetAssetData(const std::string& name, void*& ptr, size_t& size) {
auto asset = assets_.find(name);
if (asset == assets_.end()) {
return false;
}
auto data = (const char*)(mmap_root_ + asset->second.offset);
if (data[0] != 'Z' || data[1] != 'Z') {
ESP_LOGE(TAG, "The asset %s is not valid with magic %02x%02x", name.c_str(), data[0], data[1]);
return false;
}
ptr = static_cast<void*>(const_cast<char*>(data + 2));
size = asset->second.size;
return true;
}

View File

@@ -1,14 +1,19 @@
#ifndef ASSETS_H
#define ASSETS_H
#include <map>
#include <string>
#include <functional>
#include <memory>
#include <cJSON.h>
#include <esp_partition.h>
#include <model_path.h>
#include <map>
#include <string>
#if HAVE_LVGL
#include <spi_flash_mmap.h>
#endif
struct Asset {
size_t size;
@@ -28,7 +33,6 @@ public:
bool GetAssetData(const std::string& name, void*& ptr, size_t& size);
inline bool partition_valid() const { return partition_valid_; }
inline bool checksum_valid() const { return checksum_valid_; }
inline std::string default_assets_url() const { return default_assets_url_; }
private:
@@ -37,16 +41,49 @@ private:
Assets& operator=(const Assets&) = delete;
bool InitializePartition();
uint32_t CalculateChecksum(const char* data, uint32_t length);
void UnApplyPartition();
static bool FindPartition(Assets* assets);
static bool LoadSrmodelsFromIndex(Assets* assets, cJSON* root = nullptr);
class AssetStrategy {
public:
virtual ~AssetStrategy() = default;
virtual bool Apply(Assets* assets) = 0;
virtual bool InitializePartition(Assets* assets) = 0;
virtual void UnApplyPartition(Assets* assets) = 0;
virtual bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) = 0;
};
class LvglStrategy : public AssetStrategy {
public:
bool Apply(Assets* assets) override;
bool InitializePartition(Assets* assets) override;
void UnApplyPartition(Assets* assets) override;
bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) override;
private:
static uint32_t CalculateChecksum(const char* data, uint32_t length);
std::map<std::string, Asset> assets_;
esp_partition_mmap_handle_t mmap_handle_ = 0;
const char* mmap_root_ = nullptr;
bool checksum_valid_ = false;
};
class EmoteStrategy : public AssetStrategy {
public:
bool Apply(Assets* assets) override;
bool InitializePartition(Assets* assets) override;
void UnApplyPartition(Assets* assets) override;
bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) override;
};
// Strategy instance
std::unique_ptr<AssetStrategy> strategy_;
protected:
const esp_partition_t* partition_ = nullptr;
esp_partition_mmap_handle_t mmap_handle_ = 0;
const char* mmap_root_ = nullptr;
bool partition_valid_ = false;
bool checksum_valid_ = false;
std::string default_assets_url_;
srmodel_list_t* models_list_ = nullptr;
std::map<std::string, Asset> assets_;
};
#endif

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "جاري تحميل الموارد...",
"PLEASE_WAIT": "يرجى الانتظار...",
"FOUND_NEW_ASSETS": "تم العثور على موارد جديدة: %s",
"HELLO_MY_FRIEND": "مرحباً، صديقي!"
"HELLO_MY_FRIEND": "مرحباً، صديقي!",
"CONNECTION_SUCCESSFUL": "تم الاتصال بنجاح",
"FLIGHT_MODE_OFF": "وضع الطيران معطل",
"FLIGHT_MODE_ON": "وضع الطيران قيد التشغيل",
"MODEM_INIT_ERROR": "فشل تهيئة المودم"
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Намерени нови ресурси: %s",
"DOWNLOAD_ASSETS_FAILED": "Неуспешно изтегляне на ресурси",
"LOADING_ASSETS": "Зареждане на ресурси...",
"HELLO_MY_FRIEND": "Здравей, мой приятел!"
"HELLO_MY_FRIEND": "Здравей, мой приятел!",
"FLIGHT_MODE_OFF": "Режим на самолет е изключен",
"FLIGHT_MODE_ON": "Режим на самолет е включен",
"MODEM_INIT_ERROR": "Неуспешна инициализация на модема"
}
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "S'han trobat nous recursos: %s",
"DOWNLOAD_ASSETS_FAILED": "No s'han pogut descarregar els recursos",
"LOADING_ASSETS": "Carregant recursos...",
"HELLO_MY_FRIEND": "Hola, amic meu!"
"HELLO_MY_FRIEND": "Hola, amic meu!",
"FLIGHT_MODE_OFF": "El mode avió està desactivat",
"FLIGHT_MODE_ON": "El mode avió està activat",
"MODEM_INIT_ERROR": "Error d'inicialització del mòdem"
}
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "Načítání prostředků...",
"PLEASE_WAIT": "Prosím čekejte...",
"FOUND_NEW_ASSETS": "Nalezeny nové prostředky: %s",
"HELLO_MY_FRIEND": "Ahoj, můj příteli!"
"HELLO_MY_FRIEND": "Ahoj, můj příteli!",
"CONNECTION_SUCCESSFUL": "Připojení úspěšné",
"FLIGHT_MODE_OFF": "Letecký režim je vypnutý",
"FLIGHT_MODE_ON": "Letecký režim je zapnutý",
"MODEM_INIT_ERROR": "Chyba inicializace modemu"
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Fandt nye ressourcer: %s",
"DOWNLOAD_ASSETS_FAILED": "Download af ressourcer mislykkedes",
"LOADING_ASSETS": "Indlæser ressourcer...",
"HELLO_MY_FRIEND": "Hej, min ven!"
"HELLO_MY_FRIEND": "Hej, min ven!",
"FLIGHT_MODE_OFF": "Flytilstand er slukket",
"FLIGHT_MODE_ON": "Flytilstand er tændt",
"MODEM_INIT_ERROR": "Modeminitialisering mislykkedes"
}
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "Ressourcen werden geladen...",
"PLEASE_WAIT": "Bitte warten...",
"FOUND_NEW_ASSETS": "Neue Ressourcen gefunden: %s",
"HELLO_MY_FRIEND": "Hallo, mein Freund!"
"HELLO_MY_FRIEND": "Hallo, mein Freund!",
"CONNECTION_SUCCESSFUL": "Verbindung erfolgreich",
"FLIGHT_MODE_OFF": "Flugmodus ist deaktiviert",
"FLIGHT_MODE_ON": "Flugmodus ist aktiviert",
"MODEM_INIT_ERROR": "Modem-Initialisierung fehlgeschlagen"
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Βρέθηκαν νέοι πόροι: %s",
"DOWNLOAD_ASSETS_FAILED": "Αποτυχία λήψης πόρων",
"LOADING_ASSETS": "Φόρτωση πόρων...",
"HELLO_MY_FRIEND": "Γεια σου, φίλε μου!"
"HELLO_MY_FRIEND": "Γεια σου, φίλε μου!",
"FLIGHT_MODE_OFF": "Η λειτουργία πτήσης είναι απενεργοποιημένη",
"FLIGHT_MODE_ON": "Η λειτουργία πτήσης είναι ενεργή",
"MODEM_INIT_ERROR": "Αποτυχία αρχικοποίησης modem"
}
}
}

View File

@@ -11,7 +11,10 @@
"INITIALIZING": "Initializing...",
"PIN_ERROR": "Please insert SIM card",
"REG_ERROR": "Unable to access network, please check SIM card status",
"MODEM_INIT_ERROR": "Modem initialization failed",
"DETECTING_MODULE": "Detecting module...",
"FLIGHT_MODE_ON": "Flight mode is on",
"FLIGHT_MODE_OFF": "Flight mode is off",
"REGISTERING_NETWORK": "Waiting for network...",
"CHECKING_NEW_VERSION": "Checking for new version...",
"CHECK_NEW_VERSION_FAILED": "Check for new version failed, will retry in %d seconds: %s",

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "Cargando recursos...",
"PLEASE_WAIT": "Por favor espere...",
"FOUND_NEW_ASSETS": "Encontrados nuevos recursos: %s",
"HELLO_MY_FRIEND": "¡Hola, mi amigo!"
"HELLO_MY_FRIEND": "¡Hola, mi amigo!",
"CONNECTION_SUCCESSFUL": "Conexión exitosa",
"FLIGHT_MODE_OFF": "El modo avión está desactivado",
"FLIGHT_MODE_ON": "El modo avión está activado",
"MODEM_INIT_ERROR": "Error de inicialización del módem"
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "منابع جدید یافت شد: %s",
"DOWNLOAD_ASSETS_FAILED": "دانلود منابع ناموفق بود",
"LOADING_ASSETS": "بارگذاری منابع...",
"HELLO_MY_FRIEND": "سلام، دوست من!"
"HELLO_MY_FRIEND": "سلام، دوست من!",
"FLIGHT_MODE_OFF": "حالت پرواز خاموش است",
"FLIGHT_MODE_ON": "حالت پرواز روشن است",
"MODEM_INIT_ERROR": "خطا در راه‌اندازی مودم"
}
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "Ladataan resursseja...",
"PLEASE_WAIT": "Odota hetki...",
"FOUND_NEW_ASSETS": "Löydetty uusia resursseja: %s",
"HELLO_MY_FRIEND": "Hei, ystäväni!"
"HELLO_MY_FRIEND": "Hei, ystäväni!",
"CONNECTION_SUCCESSFUL": "Yhteys onnistui",
"FLIGHT_MODE_OFF": "Lentotila on pois päältä",
"FLIGHT_MODE_ON": "Lentotila on päällä",
"MODEM_INIT_ERROR": "Modeemin alustus epäonnistui"
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Nakahanap ng mga bagong assets: %s",
"DOWNLOAD_ASSETS_FAILED": "Nabigo ang pag-download ng mga assets",
"LOADING_ASSETS": "Nilo-load ang mga assets...",
"HELLO_MY_FRIEND": "Kumusta, kaibigan ko!"
"HELLO_MY_FRIEND": "Kumusta, kaibigan ko!",
"FLIGHT_MODE_OFF": "Naka-off ang flight mode",
"FLIGHT_MODE_ON": "Naka-on ang flight mode",
"MODEM_INIT_ERROR": "Nabigo ang pag-initialize ng modem"
}
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "Chargement des ressources...",
"PLEASE_WAIT": "Veuillez patienter...",
"FOUND_NEW_ASSETS": "Nouvelles ressources trouvées: %s",
"HELLO_MY_FRIEND": "Bonjour, mon ami !"
"HELLO_MY_FRIEND": "Bonjour, mon ami !",
"CONNECTION_SUCCESSFUL": "Connexion réussie",
"FLIGHT_MODE_OFF": "Le mode avion est désactivé",
"FLIGHT_MODE_ON": "Le mode avion est activé",
"MODEM_INIT_ERROR": "Échec de l'initialisation du modem"
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "נמצאו משאבים חדשים: %s",
"DOWNLOAD_ASSETS_FAILED": "הורדת משאבים נכשלה",
"LOADING_ASSETS": "טוען משאבים...",
"HELLO_MY_FRIEND": "שלום, ידידי!"
"HELLO_MY_FRIEND": "שלום, ידידי!",
"FLIGHT_MODE_OFF": "מצב טיסה כבוי",
"FLIGHT_MODE_ON": "מצב טיסה מופעל",
"MODEM_INIT_ERROR": "אתחול המודם נכשל"
}
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "संसाधन लोड हो रहे हैं...",
"PLEASE_WAIT": "कृपया प्रतीक्षा करें...",
"FOUND_NEW_ASSETS": "नए संसाधन मिले: %s",
"HELLO_MY_FRIEND": "नमस्ते, मेरे दोस्त!"
"HELLO_MY_FRIEND": "नमस्ते, मेरे दोस्त!",
"CONNECTION_SUCCESSFUL": "कनेक्शन सफल",
"FLIGHT_MODE_OFF": "फ़्लाइट मोड बंद है",
"FLIGHT_MODE_ON": "फ़्लाइट मोड चालू है",
"MODEM_INIT_ERROR": "मॉडेम आरंभीकरण विफल"
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Pronađeni novi resursi: %s",
"DOWNLOAD_ASSETS_FAILED": "Preuzimanje resursa nije uspjelo",
"LOADING_ASSETS": "Učitavanje resursa...",
"HELLO_MY_FRIEND": "Bok, moj prijatelju!"
"HELLO_MY_FRIEND": "Bok, moj prijatelju!",
"FLIGHT_MODE_OFF": "Način rada u zrakoplovu je isključen",
"FLIGHT_MODE_ON": "Način rada u zrakoplovu je uključen",
"MODEM_INIT_ERROR": "Neuspjela inicijalizacija modema"
}
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Új erőforrások találva: %s",
"DOWNLOAD_ASSETS_FAILED": "Az erőforrások letöltése sikertelen",
"LOADING_ASSETS": "Erőforrások betöltése...",
"HELLO_MY_FRIEND": "Helló, barátom!"
"HELLO_MY_FRIEND": "Helló, barátom!",
"FLIGHT_MODE_OFF": "A repülési mód ki van kapcsolva",
"FLIGHT_MODE_ON": "A repülési mód be van kapcsolva",
"MODEM_INIT_ERROR": "A modem inicializálása sikertelen"
}
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "Memuat aset...",
"PLEASE_WAIT": "Mohon tunggu...",
"FOUND_NEW_ASSETS": "Ditemukan aset baru: %s",
"HELLO_MY_FRIEND": "Halo, teman saya!"
"HELLO_MY_FRIEND": "Halo, teman saya!",
"CONNECTION_SUCCESSFUL": "Koneksi berhasil",
"FLIGHT_MODE_OFF": "Mode pesawat nonaktif",
"FLIGHT_MODE_ON": "Mode pesawat aktif",
"MODEM_INIT_ERROR": "Gagal menginisialisasi modem"
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "Caricamento risorse...",
"PLEASE_WAIT": "Attendere prego...",
"FOUND_NEW_ASSETS": "Trovate nuove risorse: %s",
"HELLO_MY_FRIEND": "Ciao, amico mio!"
"HELLO_MY_FRIEND": "Ciao, amico mio!",
"CONNECTION_SUCCESSFUL": "Connessione riuscita",
"FLIGHT_MODE_OFF": "La modalità aereo è disattivata",
"FLIGHT_MODE_ON": "La modalità aereo è attiva",
"MODEM_INIT_ERROR": "Inizializzazione modem non riuscita"
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "アセットを読み込み中...",
"PLEASE_WAIT": "お待ちください...",
"FOUND_NEW_ASSETS": "新しいアセットが見つかりました: %s",
"HELLO_MY_FRIEND": "こんにちは、友達!"
"HELLO_MY_FRIEND": "こんにちは、友達!",
"CONNECTION_SUCCESSFUL": "接続成功",
"FLIGHT_MODE_OFF": "機内モードがオフです",
"FLIGHT_MODE_ON": "機内モードがオンです",
"MODEM_INIT_ERROR": "モデムの初期化に失敗しました"
}
}

View File

@@ -51,6 +51,9 @@
"LOADING_ASSETS": "에셋 로딩 중...",
"PLEASE_WAIT": "잠시 기다려 주세요...",
"FOUND_NEW_ASSETS": "새로운 에셋을 발견했습니다: %s",
"HELLO_MY_FRIEND": "안녕하세요, 친구!"
"HELLO_MY_FRIEND": "안녕하세요, 친구!",
"FLIGHT_MODE_OFF": "비행기 모드가 꺼져 있습니다",
"FLIGHT_MODE_ON": "비행기 모드가 켜져 있습니다",
"MODEM_INIT_ERROR": "모뎀 초기화 실패"
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Menemui aset baharu: %s",
"DOWNLOAD_ASSETS_FAILED": "Gagal memuat turun aset",
"LOADING_ASSETS": "Memuatkan aset...",
"HELLO_MY_FRIEND": "Hai, kawan saya!"
"HELLO_MY_FRIEND": "Hai, kawan saya!",
"FLIGHT_MODE_OFF": "Mod penerbangan dimatikan",
"FLIGHT_MODE_ON": "Mod penerbangan dihidupkan",
"MODEM_INIT_ERROR": "Modem gagal dimulakan"
}
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Fant nye ressurser: %s",
"DOWNLOAD_ASSETS_FAILED": "Nedlasting av ressurser mislyktes",
"LOADING_ASSETS": "Laster ressurser...",
"HELLO_MY_FRIEND": "Hei, min venn!"
"HELLO_MY_FRIEND": "Hei, min venn!",
"FLIGHT_MODE_OFF": "Flymodus er av",
"FLIGHT_MODE_ON": "Flymodus er på",
"MODEM_INIT_ERROR": "Modeminitialisering mislyktes"
}
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Nieuwe bronnen gevonden: %s",
"DOWNLOAD_ASSETS_FAILED": "Downloaden van bronnen mislukt",
"LOADING_ASSETS": "Bronnen laden...",
"HELLO_MY_FRIEND": "Hallo, mijn vriend!"
"HELLO_MY_FRIEND": "Hallo, mijn vriend!",
"FLIGHT_MODE_OFF": "Vliegtuigmodus is uitgeschakeld",
"FLIGHT_MODE_ON": "Vliegtuigmodus is ingeschakeld",
"MODEM_INIT_ERROR": "Modeminitialisatie mislukt"
}
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "Ładowanie zasobów...",
"PLEASE_WAIT": "Proszę czekać...",
"FOUND_NEW_ASSETS": "Znaleziono nowe zasoby: %s",
"HELLO_MY_FRIEND": "Cześć, mój przyjacielu!"
"HELLO_MY_FRIEND": "Cześć, mój przyjacielu!",
"CONNECTION_SUCCESSFUL": "Połączenie udane",
"FLIGHT_MODE_OFF": "Tryb samolotowy jest wyłączony",
"FLIGHT_MODE_ON": "Tryb samolotowy jest włączony",
"MODEM_INIT_ERROR": "Inicjalizacja modemu nie powiodła się"
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "A carregar recursos...",
"PLEASE_WAIT": "Por favor aguarde...",
"FOUND_NEW_ASSETS": "Encontrados novos recursos: %s",
"HELLO_MY_FRIEND": "Olá, meu amigo!"
"HELLO_MY_FRIEND": "Olá, meu amigo!",
"CONNECTION_SUCCESSFUL": "Ligação bem-sucedida",
"FLIGHT_MODE_OFF": "O modo avião está desativado",
"FLIGHT_MODE_ON": "O modo avião está ativado",
"MODEM_INIT_ERROR": "Falha na inicialização do modem"
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "Se încarcă resursele...",
"PLEASE_WAIT": "Vă rugăm să așteptați...",
"FOUND_NEW_ASSETS": "S-au găsit resurse noi: %s",
"HELLO_MY_FRIEND": "Salut, prietenul meu!"
"HELLO_MY_FRIEND": "Salut, prietenul meu!",
"CONNECTION_SUCCESSFUL": "Conexiune reușită",
"FLIGHT_MODE_OFF": "Modul avion este dezactivat",
"FLIGHT_MODE_ON": "Modul avion este activat",
"MODEM_INIT_ERROR": "Inițializarea modemului a eșuat"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "Загрузка ресурсов...",
"PLEASE_WAIT": "Пожалуйста, подождите...",
"FOUND_NEW_ASSETS": "Найдены новые ресурсы: %s",
"HELLO_MY_FRIEND": "Привет, мой друг!"
"HELLO_MY_FRIEND": "Привет, мой друг!",
"CONNECTION_SUCCESSFUL": "Подключение успешно",
"FLIGHT_MODE_OFF": "Режим полета выключен",
"FLIGHT_MODE_ON": "Режим полета включен",
"MODEM_INIT_ERROR": "Ошибка инициализации модема"
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Nájdené nové zdroje: %s",
"DOWNLOAD_ASSETS_FAILED": "Sťahovanie zdrojov zlyhalo",
"LOADING_ASSETS": "Načítavanie zdrojov...",
"HELLO_MY_FRIEND": "Ahoj, môj priateľ!"
"HELLO_MY_FRIEND": "Ahoj, môj priateľ!",
"FLIGHT_MODE_OFF": "Letecký režim je vypnutý",
"FLIGHT_MODE_ON": "Letecký režim je zapnutý",
"MODEM_INIT_ERROR": "Chyba inicializácie modemu"
}
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Najdeni novi viri: %s",
"DOWNLOAD_ASSETS_FAILED": "Prenos virov ni uspel",
"LOADING_ASSETS": "Nalaganje virov...",
"HELLO_MY_FRIEND": "Pozdravljeni, moj prijatelj!"
"HELLO_MY_FRIEND": "Pozdravljeni, moj prijatelj!",
"FLIGHT_MODE_OFF": "Način leta je izklopljen",
"FLIGHT_MODE_ON": "Način leta je vklopljen",
"MODEM_INIT_ERROR": "Inicializacija modema ni uspela"
}
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Пронађени нови ресурси: %s",
"DOWNLOAD_ASSETS_FAILED": "Преузимање ресурса није успело",
"LOADING_ASSETS": "Учитавање ресурса...",
"HELLO_MY_FRIEND": "Здраво, пријатељу!"
"HELLO_MY_FRIEND": "Здраво, пријатељу!",
"FLIGHT_MODE_OFF": "Режим лета је искључен",
"FLIGHT_MODE_ON": "Режим лета је укључен",
"MODEM_INIT_ERROR": "Иницијализација модема није успела"
}
}
}

View File

@@ -51,7 +51,9 @@
"FOUND_NEW_ASSETS": "Hittade nya resurser: %s",
"DOWNLOAD_ASSETS_FAILED": "Nedladdning av resurser misslyckades",
"LOADING_ASSETS": "Laddar resurser...",
"HELLO_MY_FRIEND": "Hej, min vän!"
"HELLO_MY_FRIEND": "Hej, min vän!",
"FLIGHT_MODE_OFF": "Flygläge är av",
"FLIGHT_MODE_ON": "Flygläge är på",
"MODEM_INIT_ERROR": "Modeminitiering misslyckades"
}
}
}

View File

@@ -51,6 +51,9 @@
"LOADING_ASSETS": "กำลังโหลดทรัพยากร...",
"PLEASE_WAIT": "กรุณารอสักครู่...",
"FOUND_NEW_ASSETS": "พบทรัพยากรใหม่: %s",
"HELLO_MY_FRIEND": "สวัสดี เพื่อนของฉัน!"
"HELLO_MY_FRIEND": "สวัสดี เพื่อนของฉัน!",
"FLIGHT_MODE_OFF": "โหมดเครื่องบินปิดอยู่",
"FLIGHT_MODE_ON": "โหมดเครื่องบินเปิดอยู่",
"MODEM_INIT_ERROR": "การเริ่มต้นโมเด็มล้มเหลว"
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "Varlıklar yükleniyor...",
"PLEASE_WAIT": "Lütfen bekleyin...",
"FOUND_NEW_ASSETS": "Yeni varlıklar bulundu: %s",
"HELLO_MY_FRIEND": "Merhaba, arkadaşım!"
"HELLO_MY_FRIEND": "Merhaba, arkadaşım!",
"CONNECTION_SUCCESSFUL": "Bağlantı başarılı",
"FLIGHT_MODE_OFF": "Uçak modu kapalı",
"FLIGHT_MODE_ON": "Uçak modu açık",
"MODEM_INIT_ERROR": "Modem başlatma hatası"
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "Завантаження ресурсів...",
"PLEASE_WAIT": "Будь ласка, зачекайте...",
"FOUND_NEW_ASSETS": "Знайдено нові ресурси: %s",
"HELLO_MY_FRIEND": "Привіт, мій друже!"
"HELLO_MY_FRIEND": "Привіт, мій друже!",
"CONNECTION_SUCCESSFUL": "Підключення успішне",
"FLIGHT_MODE_OFF": "Режим польоту вимкнено",
"FLIGHT_MODE_ON": "Режим польоту увімкнено",
"MODEM_INIT_ERROR": "Помилка ініціалізації модему"
}
}

View File

@@ -51,6 +51,9 @@
"LOADING_ASSETS": "Đang tải tài nguyên...",
"PLEASE_WAIT": "Vui lòng đợi...",
"FOUND_NEW_ASSETS": "Tìm thấy tài nguyên mới: %s",
"HELLO_MY_FRIEND": "Xin chào, bạn của tôi!"
"HELLO_MY_FRIEND": "Xin chào, bạn của tôi!",
"FLIGHT_MODE_OFF": "Chế độ máy bay đang tắt",
"FLIGHT_MODE_ON": "Chế độ máy bay đang bật",
"MODEM_INIT_ERROR": "Khởi tạo modem thất bại"
}
}

View File

@@ -11,6 +11,7 @@
"INITIALIZING": "正在初始化...",
"PIN_ERROR": "请插入 SIM 卡",
"REG_ERROR": "无法接入网络,请检查流量卡状态",
"MODEM_INIT_ERROR": "模组初始化失败",
"DETECTING_MODULE": "检测模组...",
"REGISTERING_NETWORK": "等待网络...",
"CHECKING_NEW_VERSION": "检查新版本...",
@@ -50,6 +51,9 @@
"LOADING_ASSETS": "加载资源...",
"PLEASE_WAIT": "请稍候...",
"FOUND_NEW_ASSETS": "发现新资源: %s",
"HELLO_MY_FRIEND": "你好,我的朋友!"
"HELLO_MY_FRIEND": "你好,我的朋友!",
"CONNECTION_SUCCESSFUL": "连接成功",
"FLIGHT_MODE_OFF": "飞行模式已关闭",
"FLIGHT_MODE_ON": "飞行模式已开启"
}
}

View File

@@ -50,6 +50,10 @@
"LOADING_ASSETS": "載入資源...",
"PLEASE_WAIT": "請稍候...",
"FOUND_NEW_ASSETS": "發現新資源: %s",
"HELLO_MY_FRIEND": "你好,我的朋友!"
"HELLO_MY_FRIEND": "你好,我的朋友!",
"CONNECTION_SUCCESSFUL": "連線成功",
"FLIGHT_MODE_OFF": "飛航模式已關閉",
"FLIGHT_MODE_ON": "飛航模式已開啟",
"MODEM_INIT_ERROR": "模組初始化失敗"
}
}

View File

@@ -2,6 +2,26 @@
#include <esp_log.h>
#include <cstring>
#define RATE_CVT_CFG(_src_rate, _dest_rate, _channel) \
(esp_ae_rate_cvt_cfg_t) \
{ \
.src_rate = (uint32_t)(_src_rate), \
.dest_rate = (uint32_t)(_dest_rate), \
.channel = (uint8_t)(_channel), \
.bits_per_sample = ESP_AUDIO_BIT16, \
.complexity = 2, \
.perf_type = ESP_AE_RATE_CVT_PERF_TYPE_SPEED, \
}
#define OPUS_DEC_CFG(_sample_rate, _frame_duration_ms) \
(esp_opus_dec_cfg_t) \
{ \
.sample_rate = (uint32_t)(_sample_rate), \
.channel = ESP_AUDIO_MONO, \
.frame_duration = (esp_opus_dec_frame_duration_t)AS_OPUS_GET_FRAME_DRU_ENUM(_frame_duration_ms), \
.self_delimited = false, \
}
#if CONFIG_USE_AUDIO_PROCESSOR
#include "processors/afe_audio_processor.h"
#else
@@ -17,7 +37,6 @@
#define TAG "AudioService"
AudioService::AudioService() {
event_group_ = xEventGroupCreate();
}
@@ -26,21 +45,51 @@ AudioService::~AudioService() {
if (event_group_ != nullptr) {
vEventGroupDelete(event_group_);
}
if (opus_encoder_ != nullptr) {
esp_opus_enc_close(opus_encoder_);
}
if (opus_decoder_ != nullptr) {
esp_opus_dec_close(opus_decoder_);
}
if (input_resampler_ != nullptr) {
esp_ae_rate_cvt_close(input_resampler_);
}
if (output_resampler_ != nullptr) {
esp_ae_rate_cvt_close(output_resampler_);
}
}
void AudioService::Initialize(AudioCodec* codec) {
codec_ = codec;
codec_->Start();
/* Setup the audio codec */
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(codec->output_sample_rate(), 1, OPUS_FRAME_DURATION_MS);
opus_encoder_ = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
opus_encoder_->SetComplexity(0);
esp_opus_dec_cfg_t opus_dec_cfg = OPUS_DEC_CFG(codec->output_sample_rate(), OPUS_FRAME_DURATION_MS);
auto ret = esp_opus_dec_open(&opus_dec_cfg, sizeof(esp_opus_dec_cfg_t), &opus_decoder_);
if (opus_decoder_ == nullptr) {
ESP_LOGE(TAG, "Failed to create audio decoder, error code: %d", ret);
} else {
decoder_sample_rate_ = codec->output_sample_rate();
decoder_duration_ms_ = OPUS_FRAME_DURATION_MS;
decoder_frame_size_ = decoder_sample_rate_ / 1000 * OPUS_FRAME_DURATION_MS;
}
esp_opus_enc_config_t opus_enc_cfg = AS_OPUS_ENC_CONFIG();
ret = esp_opus_enc_open(&opus_enc_cfg, sizeof(esp_opus_enc_config_t), &opus_encoder_);
if (opus_encoder_ == nullptr) {
ESP_LOGE(TAG, "Failed to create audio encoder, error code: %d", ret);
} else {
encoder_sample_rate_ = 16000;
encoder_duration_ms_ = OPUS_FRAME_DURATION_MS;
esp_opus_enc_get_frame_size(opus_encoder_, &encoder_frame_size_, &encoder_outbuf_size_);
encoder_frame_size_ = encoder_frame_size_ / sizeof(int16_t);
}
if (codec->input_sample_rate() != 16000) {
input_resampler_.Configure(codec->input_sample_rate(), 16000);
reference_resampler_.Configure(codec->input_sample_rate(), 16000);
esp_ae_rate_cvt_cfg_t input_resampler_cfg = RATE_CVT_CFG(
codec->input_sample_rate(), ESP_AUDIO_SAMPLE_RATE_16K, codec->input_channels());
auto resampler_ret = esp_ae_rate_cvt_open(&input_resampler_cfg, &input_resampler_);
if (input_resampler_ == nullptr) {
ESP_LOGE(TAG, "Failed to create input resampler, error code: %d", resampler_ret);
}
}
#if CONFIG_USE_AUDIO_PROCESSOR
@@ -114,7 +163,7 @@ void AudioService::Start() {
AudioService* audio_service = (AudioService*)arg;
audio_service->OpusCodecTask();
vTaskDelete(NULL);
}, "opus_codec", 2048 * 13, this, 2, &opus_codec_task_handle_);
}, "opus_codec", 2048 * 12, this, 2, &opus_codec_task_handle_);
}
void AudioService::Stop() {
@@ -144,25 +193,16 @@ bool AudioService::ReadAudioData(std::vector<int16_t>& data, int sample_rate, in
if (!codec_->InputData(data)) {
return false;
}
if (codec_->input_channels() == 2) {
auto mic_channel = std::vector<int16_t>(data.size() / 2);
auto reference_channel = std::vector<int16_t>(data.size() / 2);
for (size_t i = 0, j = 0; i < mic_channel.size(); ++i, j += 2) {
mic_channel[i] = data[j];
reference_channel[i] = data[j + 1];
}
auto resampled_mic = std::vector<int16_t>(input_resampler_.GetOutputSamples(mic_channel.size()));
auto resampled_reference = std::vector<int16_t>(reference_resampler_.GetOutputSamples(reference_channel.size()));
input_resampler_.Process(mic_channel.data(), mic_channel.size(), resampled_mic.data());
reference_resampler_.Process(reference_channel.data(), reference_channel.size(), resampled_reference.data());
data.resize(resampled_mic.size() + resampled_reference.size());
for (size_t i = 0, j = 0; i < resampled_mic.size(); ++i, j += 2) {
data[j] = resampled_mic[i];
data[j + 1] = resampled_reference[i];
}
} else {
auto resampled = std::vector<int16_t>(input_resampler_.GetOutputSamples(data.size()));
input_resampler_.Process(data.data(), data.size(), resampled.data());
if (input_resampler_ != nullptr) {
std::lock_guard<std::mutex> lock(input_resampler_mutex_);
uint32_t in_sample_num = data.size() / codec_->input_channels();
uint32_t output_samples = 0;
esp_ae_rate_cvt_get_max_out_sample_num(input_resampler_, in_sample_num, &output_samples);
auto resampled = std::vector<int16_t>(output_samples * codec_->input_channels());
uint32_t actual_output = output_samples;
esp_ae_rate_cvt_process(input_resampler_, (esp_ae_sample_t)data.data(), in_sample_num,
(esp_ae_sample_t)resampled.data(), &actual_output);
resampled.resize(actual_output * codec_->input_channels());
data = std::move(resampled);
}
} else {
@@ -225,27 +265,18 @@ void AudioService::AudioInputTask() {
}
}
/* Feed the wake word */
if (bits & AS_EVENT_WAKE_WORD_RUNNING) {
/* Feed the wake word and/or audio processor */
if (bits & (AS_EVENT_WAKE_WORD_RUNNING | AS_EVENT_AUDIO_PROCESSOR_RUNNING)) {
int samples = 160; // 10ms
std::vector<int16_t> data;
int samples = wake_word_->GetFeedSize();
if (samples > 0) {
if (ReadAudioData(data, 16000, samples)) {
if (ReadAudioData(data, 16000, samples)) {
if (bits & AS_EVENT_WAKE_WORD_RUNNING) {
wake_word_->Feed(data);
continue;
}
}
}
/* Feed the audio processor */
if (bits & AS_EVENT_AUDIO_PROCESSOR_RUNNING) {
std::vector<int16_t> data;
int samples = audio_processor_->GetFeedSize();
if (samples > 0) {
if (ReadAudioData(data, 16000, samples)) {
if (bits & AS_EVENT_AUDIO_PROCESSOR_RUNNING) {
audio_processor_->Feed(std::move(data));
continue;
}
continue;
}
}
@@ -316,25 +347,49 @@ void AudioService::OpusCodecTask() {
task->timestamp = packet->timestamp;
SetDecodeSampleRate(packet->sample_rate, packet->frame_duration);
if (opus_decoder_->Decode(std::move(packet->payload), task->pcm)) {
// Resample if the sample rate is different
if (opus_decoder_->sample_rate() != codec_->output_sample_rate()) {
int target_size = output_resampler_.GetOutputSamples(task->pcm.size());
std::vector<int16_t> resampled(target_size);
output_resampler_.Process(task->pcm.data(), task->pcm.size(), resampled.data());
task->pcm = std::move(resampled);
if (opus_decoder_ != nullptr) {
task->pcm.resize(decoder_frame_size_);
esp_audio_dec_in_raw_t raw = {
.buffer = (uint8_t *)(packet->payload.data()),
.len = (uint32_t)(packet->payload.size()),
.consumed = 0,
.frame_recover = ESP_AUDIO_DEC_RECOVERY_NONE,
};
esp_audio_dec_out_frame_t out_frame = {
.buffer = (uint8_t *)(task->pcm.data()),
.len = (uint32_t)(task->pcm.size() * sizeof(int16_t)),
.decoded_size = 0,
};
esp_audio_dec_info_t dec_info = {};
std::unique_lock<std::mutex> decoder_lock(decoder_mutex_);
auto ret = esp_opus_dec_decode(opus_decoder_, &raw, &out_frame, &dec_info);
decoder_lock.unlock();
if (ret == ESP_AUDIO_ERR_OK) {
task->pcm.resize(out_frame.decoded_size / sizeof(int16_t));
if (decoder_sample_rate_ != codec_->output_sample_rate() && output_resampler_ != nullptr) {
uint32_t target_size = 0;
esp_ae_rate_cvt_get_max_out_sample_num(output_resampler_, task->pcm.size(), &target_size);
std::vector<int16_t> resampled(target_size);
uint32_t actual_output = target_size;
esp_ae_rate_cvt_process(output_resampler_, (esp_ae_sample_t)task->pcm.data(), task->pcm.size(),
(esp_ae_sample_t)resampled.data(), &actual_output);
resampled.resize(actual_output);
task->pcm = std::move(resampled);
}
lock.lock();
audio_playback_queue_.push_back(std::move(task));
audio_queue_cv_.notify_all();
debug_statistics_.decode_count++;
} else {
ESP_LOGE(TAG, "Failed to decode audio after resize, error code: %d", ret);
lock.lock();
}
lock.lock();
audio_playback_queue_.push_back(std::move(task));
audio_queue_cv_.notify_all();
} else {
ESP_LOGE(TAG, "Failed to decode audio");
ESP_LOGE(TAG, "Audio decoder is not configured");
lock.lock();
}
debug_statistics_.decode_count++;
}
/* Encode the audio to send queue */
if (!audio_encode_queue_.empty() && audio_send_queue_.size() < MAX_SEND_PACKETS_IN_QUEUE) {
auto task = std::move(audio_encode_queue_.front());
@@ -346,24 +401,42 @@ void AudioService::OpusCodecTask() {
packet->frame_duration = OPUS_FRAME_DURATION_MS;
packet->sample_rate = 16000;
packet->timestamp = task->timestamp;
if (!opus_encoder_->Encode(std::move(task->pcm), packet->payload)) {
ESP_LOGE(TAG, "Failed to encode audio");
continue;
}
if (task->type == kAudioTaskTypeEncodeToSendQueue) {
{
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
audio_send_queue_.push_back(std::move(packet));
if (opus_encoder_ != nullptr && task->pcm.size() == encoder_frame_size_) {
std::vector<uint8_t> buf(encoder_outbuf_size_);
esp_audio_enc_in_frame_t in = {
.buffer = (uint8_t *)(task->pcm.data()),
.len = (uint32_t)(encoder_frame_size_ * sizeof(int16_t)),
};
esp_audio_enc_out_frame_t out = {
.buffer = buf.data(),
.len = (uint32_t)encoder_outbuf_size_,
.encoded_bytes = 0,
};
auto ret = esp_opus_enc_process(opus_encoder_, &in, &out);
if (ret == ESP_AUDIO_ERR_OK) {
packet->payload.assign(buf.data(), buf.data() + out.encoded_bytes);
if (task->type == kAudioTaskTypeEncodeToSendQueue) {
{
std::lock_guard<std::mutex> lock2(audio_queue_mutex_);
audio_send_queue_.push_back(std::move(packet));
}
if (callbacks_.on_send_queue_available) {
callbacks_.on_send_queue_available();
}
} else if (task->type == kAudioTaskTypeEncodeToTestingQueue) {
std::lock_guard<std::mutex> lock2(audio_queue_mutex_);
audio_testing_queue_.push_back(std::move(packet));
}
debug_statistics_.encode_count++;
} else {
ESP_LOGE(TAG, "Failed to encode audio, error code: %d", ret);
}
if (callbacks_.on_send_queue_available) {
callbacks_.on_send_queue_available();
}
} else if (task->type == kAudioTaskTypeEncodeToTestingQueue) {
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
audio_testing_queue_.push_back(std::move(packet));
} else {
ESP_LOGE(TAG, "Failed to encode audio: encoder not configured or invalid frame size (got %u, expected %u)",
task->pcm.size(), encoder_frame_size_);
}
debug_statistics_.encode_count++;
lock.lock();
}
}
@@ -372,17 +445,38 @@ void AudioService::OpusCodecTask() {
}
void AudioService::SetDecodeSampleRate(int sample_rate, int frame_duration) {
if (opus_decoder_->sample_rate() == sample_rate && opus_decoder_->duration_ms() == frame_duration) {
if (decoder_sample_rate_ == sample_rate && decoder_duration_ms_ == frame_duration) {
return;
}
opus_decoder_.reset();
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(sample_rate, 1, frame_duration);
std::unique_lock<std::mutex> decoder_lock(decoder_mutex_);
if (opus_decoder_ != nullptr) {
esp_opus_dec_close(opus_decoder_);
opus_decoder_ = nullptr;
}
decoder_lock.unlock();
esp_opus_dec_cfg_t opus_dec_cfg = OPUS_DEC_CFG(sample_rate, frame_duration);
auto ret = esp_opus_dec_open(&opus_dec_cfg, sizeof(esp_opus_dec_cfg_t), &opus_decoder_);
if (opus_decoder_ == nullptr) {
ESP_LOGE(TAG, "Failed to create audio decoder, error code: %d", ret);
return;
}
decoder_sample_rate_ = sample_rate;
decoder_duration_ms_ = frame_duration;
decoder_frame_size_ = decoder_sample_rate_ / 1000 * frame_duration;
auto codec = Board::GetInstance().GetAudioCodec();
if (opus_decoder_->sample_rate() != codec->output_sample_rate()) {
ESP_LOGI(TAG, "Resampling audio from %d to %d", opus_decoder_->sample_rate(), codec->output_sample_rate());
output_resampler_.Configure(opus_decoder_->sample_rate(), codec->output_sample_rate());
if (decoder_sample_rate_ != codec->output_sample_rate()) {
ESP_LOGI(TAG, "Resampling audio from %d to %d", decoder_sample_rate_, codec->output_sample_rate());
if (output_resampler_ != nullptr) {
esp_ae_rate_cvt_close(output_resampler_);
output_resampler_ = nullptr;
}
esp_ae_rate_cvt_cfg_t output_resampler_cfg = RATE_CVT_CFG(
decoder_sample_rate_, codec->output_sample_rate(), ESP_AUDIO_MONO);
auto resampler_ret = esp_ae_rate_cvt_open(&output_resampler_cfg, &output_resampler_);
if (output_resampler_ == nullptr) {
ESP_LOGE(TAG, "Failed to create output resampler, error code: %d", resampler_ret);
}
}
}
@@ -390,7 +484,6 @@ void AudioService::PushTaskToEncodeQueue(AudioTaskType type, std::vector<int16_t
auto task = std::make_unique<AudioTask>();
task->type = type;
task->pcm = std::move(pcm);
/* Push the task to the encode queue */
std::unique_lock<std::mutex> lock(audio_queue_mutex_);
@@ -466,6 +559,14 @@ void AudioService::EnableWakeWordDetection(bool enable) {
}
wake_word_initialized_ = true;
}
// Reset input resampler to clear cached data from previous mode (e.g. AudioProcessor)
// This prevents buffer overflow when switching between different feed sizes
{
std::lock_guard<std::mutex> lock(input_resampler_mutex_);
if (input_resampler_ != nullptr) {
esp_ae_rate_cvt_reset(input_resampler_);
}
}
wake_word_->Start();
xEventGroupSetBits(event_group_, AS_EVENT_WAKE_WORD_RUNNING);
} else {
@@ -485,6 +586,14 @@ void AudioService::EnableVoiceProcessing(bool enable) {
/* We should make sure no audio is playing */
ResetDecoder();
audio_input_need_warmup_ = true;
// Reset input resampler to clear cached data from previous mode (e.g. WakeWord)
// This prevents buffer overflow when switching between different feed sizes
{
std::lock_guard<std::mutex> lock(input_resampler_mutex_);
if (input_resampler_ != nullptr) {
esp_ae_rate_cvt_reset(input_resampler_);
}
}
audio_processor_->Start();
xEventGroupSetBits(event_group_, AS_EVENT_AUDIO_PROCESSOR_RUNNING);
} else {
@@ -580,18 +689,16 @@ void AudioService::PlaySound(const std::string_view& ogg) {
// 解析OpusHead包
if (pkt_len >= 19 && std::memcmp(pkt_ptr, "OpusHead", 8) == 0) {
seen_head = true;
// OpusHead结构[0-7] "OpusHead", [8] version, [9] channel_count, [10-11] pre_skip
// [12-15] input_sample_rate, [16-17] output_gain, [18] mapping_family
if (pkt_len >= 12) {
uint8_t version = pkt_ptr[8];
uint8_t channel_count = pkt_ptr[9];
if (pkt_len >= 16) {
// 读取输入采样率 (little-endian)
sample_rate = pkt_ptr[12] | (pkt_ptr[13] << 8) |
sample_rate = pkt_ptr[12] | (pkt_ptr[13] << 8) |
(pkt_ptr[14] << 16) | (pkt_ptr[15] << 24);
ESP_LOGI(TAG, "OpusHead: version=%d, channels=%d, sample_rate=%d",
ESP_LOGI(TAG, "OpusHead: version=%d, channels=%d, sample_rate=%d",
version, channel_count, sample_rate);
}
}
@@ -624,9 +731,20 @@ bool AudioService::IsIdle() {
return audio_encode_queue_.empty() && audio_decode_queue_.empty() && audio_playback_queue_.empty() && audio_testing_queue_.empty();
}
void AudioService::WaitForPlaybackQueueEmpty() {
std::unique_lock<std::mutex> lock(audio_queue_mutex_);
audio_queue_cv_.wait(lock, [this]() {
return service_stopped_ || (audio_decode_queue_.empty() && audio_playback_queue_.empty());
});
}
void AudioService::ResetDecoder() {
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
opus_decoder_->ResetState();
std::unique_lock<std::mutex> decoder_lock(decoder_mutex_);
if (opus_decoder_ != nullptr) {
esp_opus_dec_reset(opus_decoder_);
}
decoder_lock.unlock();
timestamp_queue_.clear();
audio_decode_queue_.clear();
audio_playback_queue_.clear();

View File

@@ -12,10 +12,11 @@
#include <freertos/event_groups.h>
#include <esp_timer.h>
#include <model_path.h>
#include <opus_encoder.h>
#include <opus_decoder.h>
#include <opus_resampler.h>
#include "esp_audio_enc.h"
#include "esp_opus_enc.h"
#include "esp_opus_dec.h"
#include "esp_ae_rate_cvt.h"
#include "esp_audio_types.h"
#include "audio_codec.h"
#include "audio_processor.h"
@@ -46,12 +47,34 @@
#define AUDIO_POWER_TIMEOUT_MS 15000
#define AUDIO_POWER_CHECK_INTERVAL_MS 1000
#define AS_EVENT_AUDIO_TESTING_RUNNING (1 << 0)
#define AS_EVENT_WAKE_WORD_RUNNING (1 << 1)
#define AS_EVENT_AUDIO_PROCESSOR_RUNNING (1 << 2)
#define AS_EVENT_PLAYBACK_NOT_EMPTY (1 << 3)
#define AS_OPUS_GET_FRAME_DRU_ENUM(duration_ms) \
((duration_ms) == 5 ? ESP_OPUS_ENC_FRAME_DURATION_5_MS : \
(duration_ms) == 10 ? ESP_OPUS_ENC_FRAME_DURATION_10_MS : \
(duration_ms) == 20 ? ESP_OPUS_ENC_FRAME_DURATION_20_MS : \
(duration_ms) == 40 ? ESP_OPUS_ENC_FRAME_DURATION_40_MS : \
(duration_ms) == 60 ? ESP_OPUS_ENC_FRAME_DURATION_60_MS : \
(duration_ms) == 80 ? ESP_OPUS_ENC_FRAME_DURATION_80_MS : \
(duration_ms) == 100 ? ESP_OPUS_ENC_FRAME_DURATION_100_MS : \
(duration_ms) == 120 ? ESP_OPUS_ENC_FRAME_DURATION_120_MS : -1)
#define AS_OPUS_ENC_CONFIG() { \
.sample_rate = ESP_AUDIO_SAMPLE_RATE_16K, \
.channel = ESP_AUDIO_MONO, \
.bits_per_sample = ESP_AUDIO_BIT16, \
.bitrate = ESP_OPUS_BITRATE_AUTO, \
.frame_duration = (esp_opus_enc_frame_duration_t)AS_OPUS_GET_FRAME_DRU_ENUM(OPUS_FRAME_DURATION_MS), \
.application_mode = ESP_OPUS_ENC_APPLICATION_AUDIO, \
.complexity = 0, \
.enable_fec = false, \
.enable_dtx = true, \
.enable_vbr = true, \
}
struct AudioServiceCallbacks {
std::function<void(void)> on_send_queue_available;
std::function<void(const std::string&)> on_wake_word_detected;
@@ -92,6 +115,7 @@ public:
const std::string& GetLastWakeWord() const;
bool IsVoiceDetected() const { return voice_detected_; }
bool IsIdle();
void WaitForPlaybackQueueEmpty();
bool IsWakeWordRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_WAKE_WORD_RUNNING; }
bool IsAudioProcessorRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_AUDIO_PROCESSOR_RUNNING; }
bool IsAfeWakeWord();
@@ -116,11 +140,21 @@ private:
std::unique_ptr<AudioProcessor> audio_processor_;
std::unique_ptr<WakeWord> wake_word_;
std::unique_ptr<AudioDebugger> audio_debugger_;
std::unique_ptr<OpusEncoderWrapper> opus_encoder_;
std::unique_ptr<OpusDecoderWrapper> opus_decoder_;
OpusResampler input_resampler_;
OpusResampler reference_resampler_;
OpusResampler output_resampler_;
void* opus_encoder_ = nullptr;
void* opus_decoder_ = nullptr;
std::mutex decoder_mutex_;
std::mutex input_resampler_mutex_;
esp_ae_rate_cvt_handle_t input_resampler_ = nullptr;
esp_ae_rate_cvt_handle_t output_resampler_ = nullptr;
// Encoder/Decoder state
int encoder_sample_rate_ = 16000;
int encoder_duration_ms_ = OPUS_FRAME_DURATION_MS;
int encoder_frame_size_ = 0;
int encoder_outbuf_size_ = 0;
int decoder_sample_rate_ = 0;
int decoder_duration_ms_ = OPUS_FRAME_DURATION_MS;
int decoder_frame_size_ = 0;
DebugStatistics debug_statistics_;
srmodel_list_t* models_list_ = nullptr;

View File

@@ -92,7 +92,18 @@ void AfeAudioProcessor::Feed(std::vector<int16_t>&& data) {
if (afe_data_ == nullptr) {
return;
}
afe_iface_->feed(afe_data_, data.data());
std::lock_guard<std::mutex> lock(input_buffer_mutex_);
// Check running state inside lock to avoid TOCTOU race with Stop()
if (!IsRunning()) {
return;
}
input_buffer_.insert(input_buffer_.end(), data.begin(), data.end());
size_t chunk_size = afe_iface_->get_feed_chunksize(afe_data_) * codec_->input_channels();
while (input_buffer_.size() >= chunk_size) {
afe_iface_->feed(afe_data_, input_buffer_.data());
input_buffer_.erase(input_buffer_.begin(), input_buffer_.begin() + chunk_size);
}
}
void AfeAudioProcessor::Start() {
@@ -101,9 +112,12 @@ void AfeAudioProcessor::Start() {
void AfeAudioProcessor::Stop() {
xEventGroupClearBits(event_group_, PROCESSOR_RUNNING);
std::lock_guard<std::mutex> lock(input_buffer_mutex_);
if (afe_data_ != nullptr) {
afe_iface_->reset_buffer(afe_data_);
}
input_buffer_.clear();
}
bool AfeAudioProcessor::IsRunning() {

View File

@@ -9,6 +9,7 @@
#include <string>
#include <vector>
#include <functional>
#include <mutex>
#include "audio_processor.h"
#include "audio_codec.h"
@@ -37,6 +38,8 @@ private:
AudioCodec* codec_ = nullptr;
int frame_samples_ = 0;
bool is_speaking_ = false;
std::vector<int16_t> input_buffer_;
std::mutex input_buffer_mutex_;
std::vector<int16_t> output_buffer_;
void AudioProcessorTask();

View File

@@ -3,6 +3,7 @@
#include <vector>
#include <functional>
#include <atomic>
#include "audio_processor.h"
#include "audio_codec.h"
@@ -27,7 +28,7 @@ private:
int frame_samples_ = 0;
std::function<void(std::vector<int16_t>&& data)> output_callback_;
std::function<void(bool speaking)> vad_state_change_callback_;
bool is_running_ = false;
std::atomic<bool> is_running_ = false;
};
#endif

View File

@@ -1,6 +1,5 @@
#include "afe_wake_word.h"
#include "audio_service.h"
#include <esp_log.h>
#include <sstream>
@@ -100,16 +99,30 @@ void AfeWakeWord::Start() {
void AfeWakeWord::Stop() {
xEventGroupClearBits(event_group_, DETECTION_RUNNING_EVENT);
std::lock_guard<std::mutex> lock(input_buffer_mutex_);
if (afe_data_ != nullptr) {
afe_iface_->reset_buffer(afe_data_);
}
input_buffer_.clear();
}
void AfeWakeWord::Feed(const std::vector<int16_t>& data) {
if (afe_data_ == nullptr) {
return;
}
afe_iface_->feed(afe_data_, data.data());
std::lock_guard<std::mutex> lock(input_buffer_mutex_);
// Check running state inside lock to avoid TOCTOU race with Stop()
if (!(xEventGroupGetBits(event_group_) & DETECTION_RUNNING_EVENT)) {
return;
}
input_buffer_.insert(input_buffer_.end(), data.begin(), data.end());
size_t chunk_size = afe_iface_->get_feed_chunksize(afe_data_) * codec_->input_channels();
while (input_buffer_.size() >= chunk_size) {
afe_iface_->feed(afe_data_, input_buffer_.data());
input_buffer_.erase(input_buffer_.begin(), input_buffer_.begin() + chunk_size);
}
}
size_t AfeWakeWord::GetFeedSize() {
@@ -157,7 +170,7 @@ void AfeWakeWord::StoreWakeWordData(const int16_t* data, size_t samples) {
}
void AfeWakeWord::EncodeWakeWordData() {
const size_t stack_size = 4096 * 7;
const size_t stack_size = 4096 * 6;
wake_word_opus_.clear();
if (wake_word_encode_task_stack_ == nullptr) {
wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(stack_size, MALLOC_CAP_SPIRAM);
@@ -172,20 +185,62 @@ void AfeWakeWord::EncodeWakeWordData() {
auto this_ = (AfeWakeWord*)arg;
{
auto start_time = esp_timer_get_time();
auto encoder = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
encoder->SetComplexity(0); // 0 is the fastest
// Create encoder
esp_opus_enc_config_t opus_enc_cfg = AS_OPUS_ENC_CONFIG();
void* encoder_handle = nullptr;
auto ret = esp_opus_enc_open(&opus_enc_cfg, sizeof(esp_opus_enc_config_t), &encoder_handle);
if (encoder_handle == nullptr) {
ESP_LOGE(TAG, "Failed to create audio encoder, error code: %d", ret);
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
this_->wake_word_opus_.push_back(std::vector<uint8_t>());
this_->wake_word_cv_.notify_all();
return;
}
// Get frame size
int frame_size = 0;
int outbuf_size = 0;
esp_opus_enc_get_frame_size(encoder_handle, &frame_size, &outbuf_size);
frame_size = frame_size / sizeof(int16_t);
// Encode all PCM data
int packets = 0;
std::vector<int16_t> in_buffer;
esp_audio_enc_in_frame_t in = {};
esp_audio_enc_out_frame_t out = {};
for (auto& pcm: this_->wake_word_pcm_) {
encoder->Encode(std::move(pcm), [this_](std::vector<uint8_t>&& opus) {
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
this_->wake_word_opus_.emplace_back(std::move(opus));
this_->wake_word_cv_.notify_all();
});
packets++;
if (in_buffer.empty()) {
in_buffer = std::move(pcm);
} else {
in_buffer.reserve(in_buffer.size() + pcm.size());
in_buffer.insert(in_buffer.end(), pcm.begin(), pcm.end());
}
while (in_buffer.size() >= frame_size) {
std::vector<uint8_t> opus_buf(outbuf_size);
in.buffer = (uint8_t *)(in_buffer.data());
in.len = (uint32_t)(frame_size * sizeof(int16_t));
out.buffer = opus_buf.data();
out.len = outbuf_size;
out.encoded_bytes = 0;
ret = esp_opus_enc_process(encoder_handle, &in, &out);
if (ret == ESP_AUDIO_ERR_OK) {
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
this_->wake_word_opus_.emplace_back(opus_buf.data(), opus_buf.data() + out.encoded_bytes);
this_->wake_word_cv_.notify_all();
packets++;
} else {
ESP_LOGE(TAG, "Failed to encode audio, error code: %d", ret);
}
in_buffer.erase(in_buffer.begin(), in_buffer.begin() + frame_size);
}
}
this_->wake_word_pcm_.clear();
// Close encoder
esp_opus_enc_close(encoder_handle);
auto end_time = esp_timer_get_time();
ESP_LOGI(TAG, "Encode wake word opus %d packets in %ld ms", packets, (long)((end_time - start_time) / 1000));

View File

@@ -44,6 +44,8 @@ private:
std::function<void(const std::string& wake_word)> wake_word_detected_callback_;
AudioCodec* codec_ = nullptr;
std::string last_detected_wake_word_;
std::vector<int16_t> input_buffer_;
std::mutex input_buffer_mutex_;
TaskHandle_t wake_word_encode_task_ = nullptr;
StaticTask_t* wake_word_encode_task_buffer_ = nullptr;

View File

@@ -9,10 +9,8 @@
#include <esp_mn_speech_commands.h>
#include <cJSON.h>
#define TAG "CustomWakeWord"
CustomWakeWord::CustomWakeWord()
: wake_word_pcm_(), wake_word_opus_() {
}
@@ -140,49 +138,64 @@ void CustomWakeWord::Start() {
void CustomWakeWord::Stop() {
running_ = false;
std::lock_guard<std::mutex> lock(input_buffer_mutex_);
input_buffer_.clear();
}
void CustomWakeWord::Feed(const std::vector<int16_t>& data) {
if (multinet_model_data_ == nullptr || !running_) {
if (multinet_model_data_ == nullptr) {
return;
}
std::lock_guard<std::mutex> lock(input_buffer_mutex_);
// Check running state inside lock to avoid TOCTOU race with Stop()
if (!running_) {
return;
}
esp_mn_state_t mn_state;
// If input channels is 2, we need to fetch the left channel data
if (codec_->input_channels() == 2) {
auto mono_data = std::vector<int16_t>(data.size() / 2);
for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) {
mono_data[i] = data[j];
for (size_t i = 0; i < data.size(); i += 2) {
input_buffer_.push_back(data[i]);
}
StoreWakeWordData(mono_data);
mn_state = multinet_->detect(multinet_model_data_, const_cast<int16_t*>(mono_data.data()));
} else {
StoreWakeWordData(data);
mn_state = multinet_->detect(multinet_model_data_, const_cast<int16_t*>(data.data()));
input_buffer_.insert(input_buffer_.end(), data.begin(), data.end());
}
if (mn_state == ESP_MN_STATE_DETECTING) {
return;
} else if (mn_state == ESP_MN_STATE_DETECTED) {
esp_mn_results_t *mn_result = multinet_->get_results(multinet_model_data_);
for (int i = 0; i < mn_result->num && running_; i++) {
ESP_LOGI(TAG, "Custom wake word detected: command_id=%d, string=%s, prob=%f",
mn_result->command_id[i], mn_result->string, mn_result->prob[i]);
auto& command = commands_[mn_result->command_id[i] - 1];
if (command.action == "wake") {
last_detected_wake_word_ = command.text;
running_ = false;
if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_);
int chunksize = multinet_->get_samp_chunksize(multinet_model_data_);
while (input_buffer_.size() >= chunksize) {
std::vector<int16_t> chunk(input_buffer_.begin(), input_buffer_.begin() + chunksize);
StoreWakeWordData(chunk);
esp_mn_state_t mn_state = multinet_->detect(multinet_model_data_, chunk.data());
if (mn_state == ESP_MN_STATE_DETECTED) {
esp_mn_results_t *mn_result = multinet_->get_results(multinet_model_data_);
for (int i = 0; i < mn_result->num && running_; i++) {
ESP_LOGI(TAG, "Custom wake word detected: command_id=%d, string=%s, prob=%f",
mn_result->command_id[i], mn_result->string, mn_result->prob[i]);
auto& command = commands_[mn_result->command_id[i] - 1];
if (command.action == "wake") {
last_detected_wake_word_ = command.text;
running_ = false;
input_buffer_.clear();
if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_);
}
}
}
multinet_->clean(multinet_model_data_);
} else if (mn_state == ESP_MN_STATE_TIMEOUT) {
ESP_LOGD(TAG, "Command word detection timeout, cleaning state");
multinet_->clean(multinet_model_data_);
}
multinet_->clean(multinet_model_data_);
} else if (mn_state == ESP_MN_STATE_TIMEOUT) {
ESP_LOGD(TAG, "Command word detection timeout, cleaning state");
multinet_->clean(multinet_model_data_);
if (!running_) {
break;
}
input_buffer_.erase(input_buffer_.begin(), input_buffer_.begin() + chunksize);
}
}
@@ -218,20 +231,56 @@ void CustomWakeWord::EncodeWakeWordData() {
auto this_ = (CustomWakeWord*)arg;
{
auto start_time = esp_timer_get_time();
auto encoder = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
encoder->SetComplexity(0); // 0 is the fastest
// Create encoder
esp_opus_enc_config_t opus_enc_cfg = AS_OPUS_ENC_CONFIG();
void* encoder_handle = nullptr;
auto ret = esp_opus_enc_open(&opus_enc_cfg, sizeof(esp_opus_enc_config_t), &encoder_handle);
if (encoder_handle == nullptr) {
ESP_LOGE(TAG, "Failed to create audio encoder, error code: %d", ret);
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
this_->wake_word_opus_.push_back(std::vector<uint8_t>());
this_->wake_word_cv_.notify_all();
return;
}
// Get frame size
int frame_size = 0;
int outbuf_size = 0;
esp_opus_enc_get_frame_size(encoder_handle, &frame_size, &outbuf_size);
frame_size = frame_size / sizeof(int16_t);
// Encode all PCM data
int packets = 0;
std::vector<int16_t> in_buffer;
esp_audio_enc_in_frame_t in = {};
esp_audio_enc_out_frame_t out = {};
for (auto& pcm: this_->wake_word_pcm_) {
encoder->Encode(std::move(pcm), [this_](std::vector<uint8_t>&& opus) {
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
this_->wake_word_opus_.emplace_back(std::move(opus));
this_->wake_word_cv_.notify_all();
});
packets++;
if (in_buffer.empty()) {
in_buffer = std::move(pcm);
} else {
in_buffer.reserve(in_buffer.size() + pcm.size());
in_buffer.insert(in_buffer.end(), pcm.begin(), pcm.end());
}
while (in_buffer.size() >= frame_size) {
std::vector<uint8_t> opus_buf(outbuf_size);
in.buffer = (uint8_t *)(in_buffer.data());
in.len = (uint32_t)(frame_size * sizeof(int16_t));
out.buffer = opus_buf.data();
out.len = outbuf_size;
out.encoded_bytes = 0;
ret = esp_opus_enc_process(encoder_handle, &in, &out);
if (ret == ESP_AUDIO_ERR_OK) {
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
this_->wake_word_opus_.emplace_back(opus_buf.data(), opus_buf.data() + out.encoded_bytes);
this_->wake_word_cv_.notify_all();
packets++;
} else {
ESP_LOGE(TAG, "Failed to encode audio, error code: %d", ret);
}
in_buffer.erase(in_buffer.begin(), in_buffer.begin() + frame_size);
}
}
this_->wake_word_pcm_.clear();
// Close encoder
esp_opus_enc_close(encoder_handle);
auto end_time = esp_timer_get_time();
ESP_LOGI(TAG, "Encode wake word opus %d packets in %ld ms", packets, (long)((end_time - start_time) / 1000));

View File

@@ -53,6 +53,8 @@ private:
AudioCodec* codec_ = nullptr;
std::string last_detected_wake_word_;
std::atomic<bool> running_ = false;
std::vector<int16_t> input_buffer_;
std::mutex input_buffer_mutex_;
TaskHandle_t wake_word_encode_task_ = nullptr;
StaticTask_t* wake_word_encode_task_buffer_ = nullptr;

View File

@@ -54,21 +54,44 @@ void EspWakeWord::Start() {
void EspWakeWord::Stop() {
running_ = false;
std::lock_guard<std::mutex> lock(input_buffer_mutex_);
input_buffer_.clear();
}
void EspWakeWord::Feed(const std::vector<int16_t>& data) {
if (wakenet_data_ == nullptr || !running_) {
if (wakenet_data_ == nullptr) {
return;
}
int res = wakenet_iface_->detect(wakenet_data_, (int16_t *)data.data());
if (res > 0) {
last_detected_wake_word_ = wakenet_iface_->get_word_name(wakenet_data_, res);
running_ = false;
std::lock_guard<std::mutex> lock(input_buffer_mutex_);
// Check running state inside lock to avoid TOCTOU race with Stop()
if (!running_) {
return;
}
if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_);
if (codec_->input_channels() == 2) {
for (size_t i = 0; i < data.size(); i += 2) {
input_buffer_.push_back(data[i]);
}
} else {
input_buffer_.insert(input_buffer_.end(), data.begin(), data.end());
}
int chunksize = wakenet_iface_->get_samp_chunksize(wakenet_data_);
while (input_buffer_.size() >= chunksize) {
int res = wakenet_iface_->detect(wakenet_data_, input_buffer_.data());
if (res > 0) {
last_detected_wake_word_ = wakenet_iface_->get_word_name(wakenet_data_, res);
running_ = false;
input_buffer_.clear();
if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_);
}
break;
}
input_buffer_.erase(input_buffer_.begin(), input_buffer_.begin() + chunksize);
}
}

View File

@@ -9,6 +9,7 @@
#include <vector>
#include <functional>
#include <atomic>
#include <mutex>
#include "audio_codec.h"
#include "wake_word.h"
@@ -37,6 +38,8 @@ private:
std::function<void(const std::string& wake_word)> wake_word_detected_callback_;
std::string last_detected_wake_word_;
std::vector<int16_t> input_buffer_;
std::mutex input_buffer_mutex_;
};
#endif

View File

@@ -7,7 +7,6 @@
#include <esp_lcd_panel_vendor.h>
#include <esp_log.h>
#include <esp_sleep.h>
#include <wifi_station.h>
#include "application.h"
#include "button.h"
@@ -136,9 +135,9 @@ class AIPILite : public WifiBoard {
boot_button_.OnClick([this]() {
power_save_timer_->WakeUp();
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting &&
!WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});
@@ -154,7 +153,7 @@ class AIPILite : public WifiBoard {
app.SetDeviceState(kDeviceStateWifiConfiguring);
// 重置WiFi配置以确保进入配网模式
ResetWifiConfiguration();
EnterWifiConfigMode();
});
power_button_.OnClick([this]() { power_save_timer_->WakeUp(); });
@@ -236,11 +235,11 @@ class AIPILite : public WifiBoard {
return true;
}
virtual void SetPowerSaveMode(bool enabled) override {
if (!enabled) {
virtual void SetPowerSaveLevel(PowerSaveLevel level) override {
if (level != PowerSaveLevel::LOW_POWER) {
power_save_timer_->WakeUp();
}
WifiBoard::SetPowerSaveMode(enabled);
WifiBoard::SetPowerSaveLevel(level);
}
};

View File

@@ -8,7 +8,6 @@
#include "led/single_led.h"
#include "i2c_device.h"
#include <wifi_station.h>
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <freertos/FreeRTOS.h>
@@ -244,8 +243,9 @@ private:
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});

View File

@@ -13,7 +13,6 @@
#include "i2c_device.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <wifi_station.h>
#include <driver/rtc_io.h>
#include <esp_sleep.h>
@@ -227,8 +226,9 @@ private:
middle_button_.OnLongPress([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
if (app.GetDeviceState() != kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
@@ -377,11 +377,11 @@ public:
return true;
}
virtual void SetPowerSaveMode(bool enabled) override {
if (!enabled) {
virtual void SetPowerSaveLevel(PowerSaveLevel level) override {
if (level != PowerSaveLevel::LOW_POWER) {
power_save_timer_->WakeUp();
}
WifiBoard::SetPowerSaveMode(enabled);
WifiBoard::SetPowerSaveLevel(level);
}
};

View File

@@ -13,7 +13,6 @@
#include "i2c_device.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <wifi_station.h>
#include <driver/rtc_io.h>
#include <esp_sleep.h>
@@ -280,9 +279,10 @@ private:
auto& app = Application::GetInstance();
if (self->GetNetworkType() == NetworkType::WIFI) {
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
if (app.GetDeviceState() == kDeviceStateStarting) {
auto& wifi_board = static_cast<WifiBoard&>(self->GetCurrentBoard());
wifi_board.ResetWifiConfiguration();
wifi_board.EnterWifiConfigMode();
return;
}
}
@@ -463,11 +463,11 @@ public:
return true;
}
virtual void SetPowerSaveMode(bool enabled) override {
if (!enabled) {
virtual void SetPowerSaveLevel(PowerSaveLevel level) override {
if (level != PowerSaveLevel::LOW_POWER) {
power_save_timer_->WakeUp();
}
DualNetworkBoard::SetPowerSaveMode(enabled);
DualNetworkBoard::SetPowerSaveLevel(level);
}
};

View File

@@ -13,7 +13,6 @@
#include "i2c_device.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <wifi_station.h>
#include <driver/rtc_io.h>
#include <esp_sleep.h>
@@ -262,8 +261,9 @@ private:
auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data);
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
self->ResetWifiConfiguration();
if (app.GetDeviceState() == kDeviceStateStarting) {
self->EnterWifiConfigMode();
return;
}
if (self->power_status_ == kDeviceBatterySupply) {
@@ -442,11 +442,11 @@ public:
return true;
}
virtual void SetPowerSaveMode(bool enabled) override {
if (!enabled) {
virtual void SetPowerSaveLevel(PowerSaveLevel level) override {
if (level != PowerSaveLevel::LOW_POWER) {
power_save_timer_->WakeUp();
}
WifiBoard::SetPowerSaveMode(enabled);
WifiBoard::SetPowerSaveLevel(level);
}
};

View File

@@ -6,13 +6,12 @@
#include "config.h"
#include "i2c_device.h"
#include "led/single_led.h"
#include "esp32_camera.h"
#include "esp_video.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <driver/i2c_master.h>
#include <driver/spi_common.h>
#include <wifi_station.h>
#define TAG "atk_dnesp32s3"
@@ -50,7 +49,7 @@ private:
Button boot_button_;
LcdDisplay* display_;
XL9555* xl9555_;
Esp32Camera* camera_;
EspVideo* camera_;
void InitializeI2c() {
// Initialize I2C peripheral
@@ -87,8 +86,9 @@ private:
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});
@@ -179,7 +179,7 @@ private:
.dvp = &dvp_config,
};
camera_ = new Esp32Camera(video_config);
camera_ = new EspVideo(video_config);
}
public:

View File

@@ -9,7 +9,6 @@
#include "driver/gpio.h"
#include "assets/lang_config.h"
#include <wifi_station.h>
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <esp_lcd_panel_vendor.h>
@@ -60,8 +59,9 @@ private:
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});

View File

@@ -8,7 +8,6 @@
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <wifi_station.h>
#define TAG "AtomEchoS3R"
@@ -57,8 +56,9 @@ private:
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});

View File

@@ -7,7 +7,6 @@
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <wifi_station.h>
#include "led/circular_strip.h"
#define TAG "XX+EchoBase"
@@ -91,8 +90,9 @@ private:
ESP_LOGI(TAG, " ===>>> face_button_.OnClick ");
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});

View File

@@ -9,7 +9,6 @@
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <wifi_station.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_gc9a01.h>
@@ -180,8 +179,9 @@ private:
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});

View File

@@ -14,12 +14,6 @@ AtomS3R CAM、AtomS3R M12 是 M5Stack 推出的基于 ESP32-S3-PICO-1-N8R8 的
两款开发版均**不带屏幕、不带额外按键**,需要使用语音唤醒。必要时,需要使用 `idf.py monitor` 查看 log 以确定运行状态。
> ![NOTE]
>
> 自版本 [待定] 起,由于依赖库不支持 OV3660 传感器AtomS3R M12 无法使用摄像头识别功能。
>
> AtomS3R CAM 不受影响;使用旧版本小智固件的 AtomS3R M12 不受影响。
## 配置、编译命令
**配置编译目标为 ESP32S3**

View File

@@ -8,7 +8,6 @@
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <wifi_station.h>
#include "esp32_camera.h"
#define TAG "AtomS3R CAM/M12 + EchoBase"
@@ -127,47 +126,33 @@ private:
}
void InitializeCamera() {
static esp_cam_ctlr_dvp_pin_config_t dvp_pin_config = {
.data_width = CAM_CTLR_DATA_WIDTH_8,
.data_io = {
[0] = CAMERA_PIN_D0,
[1] = CAMERA_PIN_D1,
[2] = CAMERA_PIN_D2,
[3] = CAMERA_PIN_D3,
[4] = CAMERA_PIN_D4,
[5] = CAMERA_PIN_D5,
[6] = CAMERA_PIN_D6,
[7] = CAMERA_PIN_D7,
},
.vsync_io = CAMERA_PIN_VSYNC,
.de_io = CAMERA_PIN_HREF,
.pclk_io = CAMERA_PIN_PCLK,
.xclk_io = CAMERA_PIN_XCLK,
};
camera_config_t config = {};
config.pin_d0 = CAMERA_PIN_D0;
config.pin_d1 = CAMERA_PIN_D1;
config.pin_d2 = CAMERA_PIN_D2;
config.pin_d3 = CAMERA_PIN_D3;
config.pin_d4 = CAMERA_PIN_D4;
config.pin_d5 = CAMERA_PIN_D5;
config.pin_d6 = CAMERA_PIN_D6;
config.pin_d7 = CAMERA_PIN_D7;
config.pin_xclk = CAMERA_PIN_XCLK;
config.pin_pclk = CAMERA_PIN_PCLK;
config.pin_vsync = CAMERA_PIN_VSYNC;
config.pin_href = CAMERA_PIN_HREF;
config.pin_sccb_sda = CAMERA_PIN_SIOD;
config.pin_sccb_scl = CAMERA_PIN_SIOC;
config.sccb_i2c_port = 1;
config.pin_pwdn = CAMERA_PIN_PWDN;
config.pin_reset = CAMERA_PIN_RESET;
config.xclk_freq_hz = XCLK_FREQ_HZ;
config.pixel_format = PIXFORMAT_RGB565;
config.frame_size = FRAMESIZE_QVGA;
config.jpeg_quality = 12;
config.fb_count = 1;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
esp_video_init_sccb_config_t sccb_config = {
.init_sccb = true,
.i2c_config = {
.port = 1,
.scl_pin = CAMERA_PIN_SIOC,
.sda_pin = CAMERA_PIN_SIOD,
},
.freq = 100000,
};
esp_video_init_dvp_config_t dvp_config = {
.sccb_config = sccb_config,
.reset_pin = CAMERA_PIN_RESET,
.pwdn_pin = CAMERA_PIN_PWDN,
.dvp_pin = dvp_pin_config,
.xclk_freq = XCLK_FREQ_HZ,
};
esp_video_init_config_t video_config = {
.dvp = &dvp_config,
};
camera_ = new Esp32Camera(video_config);
camera_ = new Esp32Camera(config);
camera_->SetHMirror(false);
}

View File

@@ -5,10 +5,7 @@
"name": "atoms3r-cam-m12-echo-base",
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\"",
"CONFIG_CAMERA_GC0308=y",
"CONFIG_CAMERA_GC0308_AUTO_DETECT_DVP_INTERFACE_SENSOR=y",
"CONFIG_CAMERA_GC0308_DVP_YUV422_320X240_20FPS=y"
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""
]
}
]

View File

@@ -9,7 +9,6 @@
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <wifi_station.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_gc9a01.h>
@@ -258,8 +257,9 @@ private:
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});

View File

@@ -1,4 +1,4 @@
#include "dual_network_board.h"
#include "wifi_board.h"
#include "codecs/no_audio_codec.h"
#include "display/lcd_display.h"
#include "system_reset.h"
@@ -7,7 +7,6 @@
#include "config.h"
#include "led/single_led.h"
#include <wifi_station.h>
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <esp_lcd_panel_vendor.h>
@@ -58,7 +57,7 @@ static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = {
#define TAG "ESP32-LCD-MarsbearSupport"
class CompactWifiBoardLCD : public DualNetworkBoard {
class CompactWifiBoardLCD : public WifiBoard {
private:
Button boot_button_;
Button touch_button_;
@@ -137,25 +136,14 @@ private:
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (GetNetworkType() == NetworkType::WIFI) {
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
// cast to WifiBoard
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
wifi_board.ResetWifiConfiguration();
}
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
gpio_set_level(BUILTIN_LED_GPIO, 1);
app.ToggleChatState();
});
boot_button_.OnDoubleClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
SwitchNetworkType();
}
});
asr_button_.OnClick([this]() {
std::string wake_word="你好小智";
Application::GetInstance().WakeWordInvoke(wake_word);
@@ -174,8 +162,7 @@ private:
}
public:
CompactWifiBoardLCD() :
DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN),
CompactWifiBoardLCD() : WifiBoard(),
boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO) {
InitializeSpi();
InitializeLcdDisplay();

View File

@@ -45,7 +45,7 @@
#elif CONFIG_OLED_SSD1306_128X64
#define DISPLAY_HEIGHT 64
#else
#error "未选择 OLED 屏幕类型"
#error "OLED display type is not selected"
#endif
#define DISPLAY_MIRROR_X true

View File

@@ -1,4 +1,4 @@
#include "dual_network_board.h"
#include "wifi_board.h"
#include "codecs/no_audio_codec.h"
#include "system_reset.h"
#include "application.h"
@@ -9,7 +9,6 @@
#include "led/single_led.h"
#include "display/oled_display.h"
#include <wifi_station.h>
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <esp_lcd_panel_ops.h>
@@ -17,7 +16,7 @@
#define TAG "ESP32-MarsbearSupport"
class CompactWifiBoard : public DualNetworkBoard {
class CompactWifiBoard : public WifiBoard {
private:
Button boot_button_;
Button touch_button_;
@@ -105,25 +104,14 @@ private:
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (GetNetworkType() == NetworkType::WIFI) {
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
// cast to WifiBoard
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
wifi_board.ResetWifiConfiguration();
}
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
gpio_set_level(BUILTIN_LED_GPIO, 1);
app.ToggleChatState();
});
boot_button_.OnDoubleClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
SwitchNetworkType();
}
});
asr_button_.OnClick([this]() {
std::string wake_word="你好小智";
Application::GetInstance().WakeWordInvoke(wake_word);
@@ -145,7 +133,7 @@ private:
}
public:
CompactWifiBoard() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO)
CompactWifiBoard() : WifiBoard(), boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO)
{
InitializeDisplayI2c();
InitializeSsd1306Display();

View File

@@ -14,7 +14,6 @@
#include <driver/i2c_master.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_vendor.h>
#include <wifi_station.h>
#define TAG "CompactMl307Board"
@@ -96,10 +95,11 @@ private:
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (GetNetworkType() == NetworkType::WIFI) {
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
if (app.GetDeviceState() == kDeviceStateStarting) {
// cast to WifiBoard
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
wifi_board.ResetWifiConfiguration();
wifi_board.EnterWifiConfigMode();
return;
}
}
app.ToggleChatState();

Some files were not shown because too many files have changed in this diff Show More