Compare commits

...

11 Commits

Author SHA1 Message Date
417f52d759 perf(websocket): switch WiFi to performance mode before connecting (#1985)
Some checks failed
Build Boards / Determine variants to build (push) Has been cancelled
Build Boards / Build ${{ matrix.full_name }} (push) Has been cancelled
* perf(websocket): switch WiFi to performance mode before connecting

Optimize WebSocket connection speed by switching WiFi to performance
mode before establishing the connection, instead of after.

This reduces network latency significantly:
- TCP connection: 1093ms → 88ms (92% faster)
- WebSocket handshake: 1035ms → 80ms (92% faster)
- Total network layer: 2128ms → 173ms (92% faster)

The issue was caused by WiFi power save mode (MAX_MODEM) which adds
significant latency to packet transmission.

* Adjust formatting
2026-05-14 14:36:14 +08:00
67bf599149 fix(otto): WebSocket direct clients not receiving MCP responses (#1992)
* Enhance Otto Robot camera support by adding configuration for OV3660. Updated config.h to define camera types and GPIO settings, modified config.json to include new camera options, and refactored otto_robot.cc for improved camera detection and initialization logic.

* fix: 移除 OttoEmojiDisplay 构造函数中的 SetTheme 调用以修复 LoadProhibited 崩溃

Made-with: Cursor

* refactor: improve audio service error handling and codec timeout management

- Updated AudioService to prevent input task termination on read timeout, introducing a delay instead.
- Enhanced NoAudioCodec to implement a read timeout for I2S channel reads.
- Adjusted WebSocketControlServer to set a control port for improved socket management.
- Added manufacturer information to the config.json for waveshare ESP32-Touch-LCD-3.5.

* fix(otto): WebSocket direct clients not receiving MCP responses

When a browser connects directly to the WebSocket control server (port
8080) and sends a JSON-RPC request, the MCP response was routed through
Application::SendMcpMessage -> protocol_->SendMcpMessage, which sends it
to the cloud protocol channel. As a result, the direct WebSocket client
never received the response, while the WeChat mini-program could because
it communicates via the cloud.

Fix:
- Add BroadcastMessage() to WebSocketControlServer, using
  httpd_queue_work + httpd_ws_send_frame_async to asynchronously
  send responses back to all connected clients on port 8080
- Add RegisterMcpBroadcastCallback() to Application, allowing an
  additional MCP send callback to be registered; SendMcpMessage()
  now invokes it alongside the cloud protocol
- Register the broadcast callback in OttoRobot after the WebSocket
  server starts successfully

Also add WebSocket direct-connect API documentation to README.md
with complete JSON-RPC 2.0 command examples.
2026-05-14 14:35:49 +08:00
ba27c12494 fix(no_audio_codec): replace kReadTimeoutTicks with kReadTimeoutMs for clarity and consistency 2026-05-07 22:11:42 +08:00
c1d520d700 Feat: Add battery support and small fixes for Freenove 2.8 board (#1976)
* feat(freenove-esp32s3): add battery level retrieval

* fix(freenove-esp32s3): add missing comma in config.json

* docs(freenove-esp32s3): note possible shared design with ES3C28P/ES3N28P
2026-05-07 20:51:58 +08:00
1847b58935 fix(mcp): remove unnecessary guard for self.assets.set_download_url tool registration
The guard around the registration of the self.assets.set_download_url tool has been removed, ensuring it is always available for configuration. This change addresses issues on 32MB flash devices where the tool was previously skipped due to partition validation checks.

Fixes #1962
2026-05-02 15:56:47 +08:00
2be3c2cb1a fix(mcp): always register self.assets.set_download_url tool for 32MB flash devices (#1971)
* fix(m5stack-tab5): remove stale esp_video==0.7.0 dependency instructions

The README previously instructed users to override esp_video to 0.7.0
and esp_ipa to 0.1.0, but this causes build failures because:
- esp_video 0.7.0 does not export esp_video_deinit(), resulting in
  linker errors ('MAP_FAILED' and 'esp_video_deinit' not declared)
- The project's main/idf_component.yml already pins the correct
  version (esp_video==1.3.1) that the source code expects

Users should now use the default dependency versions from idf_component.yml
without modification.

Fixes #1957

* fix(mcp): always register self.assets.set_download_url tool

On 32MB flash devices the assets partition layout differs from the
default, causing partition_valid() to return false and silently
skipping registration of the self.assets.set_download_url MCP tool.
Users see 'Unknown tool: self.assets.set_download_url' from their MCP
client.

The tool writes to Settings storage which works regardless of the
partition map, so the partition_valid() guard is unnecessary.
Move the AddUserOnlyTool call outside the guard so the tool is always
available for explicit configuration via MCP.

Fixes #1962

---------

Co-authored-by: Aayush Pratap Singh <aayushpratap.singh@gmail.com>
2026-05-02 06:23:25 +08:00
e12e7351d9 Merge pull request #1958 from rymcu/main
feat(board): add rymcu-bigsmart board support
2026-04-30 17:16:40 +08:00
20175fa059 Move RYMCU BigSmart under manufacturer directory
Create main/boards/rymcu/bigsmart so future RYMCU boards can live under the same manufacturer directory. Update CMake to set MANUFACTURER to rymcu while preserving BOARD_NAME as rymcu-bigsmart, and adjust config.json so release output remains rymcu-bigsmart.
2026-04-30 16:04:30 +08:00
8cbbf3f357 chore: update dependencies in idf_component.yml
- Bump esp-wifi-connect version from ~3.1.2 to ~3.1.3
- Update uart-eth-modem version from ~0.3.4 to ~0.4.0
2026-04-30 13:29:36 +08:00
79a482a09e fix(blufi): GET_WIFI_LIST triggers real-time scan with guaranteed response (#1964)
* fix(blufi): GET_WIFI_LIST triggers real-time scan with guaranteed response

Previously, ESP_BLUFI_EVENT_GET_WIFI_LIST waited for any in-progress scan
to finish and then returned the cached result. When the cache was empty
(e.g. after a config-mode transition that stopped the Wi-Fi driver),
_send_wifi_list() returned silently with no response frame, leaving the
App waiting until timeout.

Changes:
- GET_WIFI_LIST now clears the cache and starts a fresh scan immediately.
- _wifi_scan_event_handler calls _send_wifi_list() after every scan
  triggered by a GET_WIFI_LIST request.
- start_wifi_scan() calls esp_wifi_start() before esp_wifi_scan_start()
  to handle the case where the driver was stopped during a mode
  transition (ESP_ERR_WIFI_STATE is treated as already-started).
- _send_wifi_list() sends ESP_BLUFI_WIFI_SCAN_FAIL when no APs are
  found, so the App always receives a terminal response.
- Redundant static_cast in _wifi_scan_event_handler replaced with the
  existing local `self` pointer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(blufi): preserve cache fast-path, fall back to live scan only when needed

Address review feedback on always-rescan latency regression.

The GET_WIFI_LIST handler now distinguishes three cases:

1. Scan in flight: defer the response via m_send_list_after_scan; the
   scan-done handler dispatches when it fires. Removes the previous
   blocking `while (m_scan_in_progress) vTaskDelay(500)` which would
   stall the BluFi event task indefinitely if the scan never completed.

2. Cache populated: respond from cache immediately (~50 ms, no latency
   change vs original behavior). _send_wifi_list() still kicks off an
   async refresh scan as before to keep the cache fresh.

3. Cache empty and no scan running: trigger a live scan and dispatch
   from the scan-done handler. If start_wifi_scan() fails, send
   ESP_BLUFI_WIFI_SCAN_FAIL so the App exits its wait state.

State variables are also disentangled:

- m_scan_should_save_ssid keeps its original meaning (write scan results
  into m_ap_records). Cleared during connect-to-AP so the connect-time
  scan does not pollute the cache.
- m_send_list_after_scan is new and tracks "the next scan-done event
  should respond to a pending GET_WIFI_LIST request". The previous PR
  conflated these two responsibilities onto m_scan_should_save_ssid,
  which would have caused init-time scans to spuriously emit a wifi list
  to the App.

start_wifi_scan() now returns bool so the caller can distinguish
"scan started or already running" from "could not start a scan".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Yixin Shi <shiyixin@qiniu.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 09:27:49 +08:00
07e2a11253 feat(board): add rymcu-bigsmart board support 2026-04-23 20:44:35 +08:00
22 changed files with 794 additions and 54 deletions

View File

@ -158,6 +158,13 @@ elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV_C3)
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_RYMCU_BIGSMART)
set(MANUFACTURER "rymcu")
set(BOARD_TYPE "bigsmart")
set(BOARD_NAME "rymcu-bigsmart")
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_EDA_TV_PRO)
set(MANUFACTURER "lceda-course-examples")
set(BOARD_TYPE "eda-tv-pro")

View File

@ -215,6 +215,9 @@ choice BOARD_TYPE
config BOARD_TYPE_LICHUANG_DEV_C3
bool "立创·实战派 ESP32-C3"
depends on IDF_TARGET_ESP32C3
config BOARD_TYPE_RYMCU_BIGSMART
bool "RYMCU BigSmart"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_EDA_TV_PRO
bool "EDA课程案例 EDA-TV-Pro"
depends on IDF_TARGET_ESP32S3
@ -711,7 +714,7 @@ choice DISPLAY_STYLE
config USE_EMOTE_MESSAGE_STYLE
bool "Emote animation style"
depends on BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_3 \
|| BOARD_TYPE_ESP_VOCAT || BOARD_TYPE_LICHUANG_DEV_S3 \
|| BOARD_TYPE_ESP_VOCAT || BOARD_TYPE_LICHUANG_DEV_S3 || BOARD_TYPE_RYMCU_BIGSMART \
|| BOARD_TYPE_ESP_SENSAIRSHUTTLE
endchoice
@ -808,7 +811,7 @@ config USE_DEVICE_AEC
bool "Enable Device-Side AEC"
default n
depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE \
|| BOARD_TYPE_LICHUANG_DEV_S3 || BOARD_TYPE_ESP_KORVO2_V3 || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_75|| BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_75C || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_83\
|| BOARD_TYPE_LICHUANG_DEV_S3 || BOARD_TYPE_RYMCU_BIGSMART || BOARD_TYPE_ESP_KORVO2_V3 || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_75|| BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_75C || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_83\
|| BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_2_06 || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_4B || BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4B || BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4_3 \
|| BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_7B \
|| BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_3_4C || BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4C || BOARD_TYPE_ESP_S3_LCD_EV_Board_2 || BOARD_TYPE_YUNLIAO_S3 \

View File

@ -716,6 +716,10 @@ void Application::ContinueOpenAudioChannel(ListeningMode mode) {
return;
}
// Switch to performance mode before connecting to reduce latency
auto& board = Board::GetInstance();
board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE);
if (!protocol_->IsAudioChannelOpened()) {
if (!protocol_->OpenAudioChannel()) {
return;
@ -825,6 +829,10 @@ void Application::ContinueWakeWordInvoke(const std::string& wake_word) {
return;
}
// Switch to performance mode before connecting to reduce latency
auto& board = Board::GetInstance();
board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE);
if (!protocol_->IsAudioChannelOpened()) {
if (!protocol_->OpenAudioChannel()) {
audio_service_.EnableWakeWordDetection(true);
@ -1063,12 +1071,19 @@ bool Application::CanEnterSleepMode() {
return true;
}
void Application::RegisterMcpBroadcastCallback(std::function<void(const std::string&)> callback) {
mcp_broadcast_callback_ = std::move(callback);
}
void Application::SendMcpMessage(const std::string& payload) {
// Always schedule to run in main task for thread safety
Schedule([this, payload = std::move(payload)]() {
Schedule([this, payload](){
if (protocol_) {
protocol_->SendMcpMessage(payload);
}
if (mcp_broadcast_callback_) {
mcp_broadcast_callback_(payload);
}
});
}

View File

@ -10,6 +10,7 @@
#include <mutex>
#include <deque>
#include <memory>
#include <functional>
#include "protocol.h"
#include "ota.h"
@ -108,6 +109,7 @@ public:
bool UpgradeFirmware(const std::string& url, const std::string& version = "");
bool CanEnterSleepMode();
void SendMcpMessage(const std::string& payload);
void RegisterMcpBroadcastCallback(std::function<void(const std::string&)> callback);
void SetAecMode(AecMode mode);
AecMode GetAecMode() const { return aec_mode_; }
void PlaySound(const std::string_view& sound);
@ -136,6 +138,8 @@ private:
AudioService audio_service_;
std::unique_ptr<Ota> ota_;
std::function<void(const std::string&)> mcp_broadcast_callback_;
bool has_server_time_ = false;
bool aborted_ = false;
bool assets_version_checked_ = false;

View File

@ -2,6 +2,7 @@
#include <esp_log.h>
#include <cmath>
#include <cstdint>
#include <cstring>
#define TAG "NoAudioCodec"
@ -239,10 +240,10 @@ int NoAudioCodec::Write(const int16_t* data, int samples) {
int NoAudioCodec::Read(int16_t* dest, int samples) {
size_t bytes_read;
constexpr TickType_t kReadTimeoutTicks = pdMS_TO_TICKS(200);
constexpr uint32_t kReadTimeoutMs = 200;
std::vector<int32_t> bit32_buffer(samples);
if (i2s_channel_read(rx_handle_, bit32_buffer.data(), samples * sizeof(int32_t), &bytes_read, kReadTimeoutTicks) != ESP_OK) {
if (i2s_channel_read(rx_handle_, bit32_buffer.data(), samples * sizeof(int32_t), &bytes_read, kReadTimeoutMs) != ESP_OK) {
return 0;
}

View File

@ -533,13 +533,13 @@ int Blufi::_get_softap_conn_num() {
return 0;
}
void Blufi::start_wifi_scan() {
bool Blufi::start_wifi_scan() {
ESP_LOGI(BLUFI_TAG, "Starting dedicated WiFi scan");
// Check if a scan is already in progress
// Already running: caller can rely on the in-flight scan and await its done event.
if (m_scan_in_progress) {
ESP_LOGW(BLUFI_TAG, "Scan already in progress, skipping");
return;
return true;
}
m_scan_in_progress = true;
@ -555,14 +555,14 @@ void Blufi::start_wifi_scan() {
if (err != ESP_OK) {
ESP_LOGE(BLUFI_TAG, "Failed to set WiFi mode to STA: %s", esp_err_to_name(err));
m_scan_in_progress = false;
return;
return false;
}
// Need to restart WiFi for mode change to take effect
err = esp_wifi_start();
if (err != ESP_OK) {
ESP_LOGE(BLUFI_TAG, "Failed to start WiFi after mode switch: %s", esp_err_to_name(err));
m_scan_in_progress = false;
return;
return false;
}
// Register scan event handler
esp_event_handler_instance_t scan_event_instance;
@ -575,28 +575,36 @@ void Blufi::start_wifi_scan() {
if (err != ESP_OK) {
ESP_LOGE(BLUFI_TAG, "Failed to start WiFi scan: %s", esp_err_to_name(err));
m_scan_in_progress = false;
return;
return false;
}
} else if (current_mode == WIFI_MODE_STA || current_mode == WIFI_MODE_APSTA) {
// Ensure WiFi driver is started (may have been stopped during config mode transition)
err = esp_wifi_start();
if (err != ESP_OK && err != ESP_ERR_WIFI_STATE) {
ESP_LOGE(BLUFI_TAG, "Failed to start WiFi before scan: %s", esp_err_to_name(err));
m_scan_in_progress = false;
return false;
}
} else if (current_mode == WIFI_MODE_STA) {
// Start scan
err = esp_wifi_scan_start(NULL, false);
if (err != ESP_OK) {
ESP_LOGE(BLUFI_TAG, "Failed to start WiFi scan: %s", esp_err_to_name(err));
m_scan_in_progress = false;
return;
return false;
}
} else {
ESP_LOGE(BLUFI_TAG, "Unexpected WiFi mode: %d", current_mode);
m_scan_in_progress = false;
return;
return false;
}
ESP_LOGI(BLUFI_TAG, "WiFi scan started");
return true;
}
void Blufi::_send_wifi_list() {
if (m_ap_records.empty()) {
ESP_LOGW(BLUFI_TAG, "No AP records available to send");
ESP_LOGW(BLUFI_TAG, "No AP records available, sending WiFi scan fail");
esp_blufi_send_error_info(ESP_BLUFI_WIFI_SCAN_FAIL);
return;
}
@ -631,7 +639,7 @@ void Blufi::_wifi_scan_event_handler(void* arg, esp_event_base_t event_base, int
ESP_LOGW(BLUFI_TAG, "No APs found");
self->m_ap_records.clear();
} else {
if (static_cast<Blufi*>(arg)->m_scan_should_save_ssid == true) {
if (self->m_scan_should_save_ssid) {
self->m_ap_records.resize(ap_num);
esp_wifi_scan_get_ap_records(&ap_num, self->m_ap_records.data());
@ -643,6 +651,11 @@ void Blufi::_wifi_scan_event_handler(void* arg, esp_event_base_t event_base, int
}
}
self->m_scan_in_progress = false;
// Dispatch a pending GET_WIFI_LIST response if one is waiting on this scan.
if (self->m_send_list_after_scan) {
self->m_send_list_after_scan = false;
self->_send_wifi_list();
}
}
}
@ -865,10 +878,29 @@ void Blufi::_handle_event(esp_blufi_cb_event_t event, esp_blufi_cb_param_t* para
break;
case ESP_BLUFI_EVENT_GET_WIFI_LIST: {
ESP_LOGI(BLUFI_TAG, "BLUFI get wifi list");
while (m_scan_in_progress) {
vTaskDelay(pdMS_TO_TICKS(500));
// Case 1: a scan is already in flight (init scan or refresh scan started by
// the previous _send_wifi_list()). Defer the response to its done handler
// instead of blocking the BluFi task.
if (m_scan_in_progress) {
m_send_list_after_scan = true;
break;
}
// Case 2: cache is populated. Respond immediately; _send_wifi_list() also
// kicks off an async refresh scan to keep the cache fresh.
if (!m_ap_records.empty()) {
_send_wifi_list();
break;
}
// Case 3: no cache (e.g. driver was stopped during a config-mode transition,
// init scan never completed). Trigger a real scan and dispatch from the
// scan-done handler. If the scan cannot start, return an error frame so the
// App exits its wait state instead of timing out.
m_scan_should_save_ssid = true;
m_send_list_after_scan = true;
if (!start_wifi_scan()) {
m_send_list_after_scan = false;
esp_blufi_send_error_info(ESP_BLUFI_WIFI_SCAN_FAIL);
}
_send_wifi_list();
break;
}
default:

View File

@ -25,8 +25,9 @@ public:
* This method intelligently handles WiFi scanning based on current WiFi state:
* - If WiFi config mode is active, it uses the existing scan results from WifiConfigurationAp
* - Otherwise, it performs a dedicated scan without interfering with normal WiFi operations
* @return true if a scan was started (or was already in progress); false on failure.
*/
void start_wifi_scan();
bool start_wifi_scan();
/**
* @brief Initializes the Bluetooth controller, host, and Blufi profile.
@ -143,5 +144,13 @@ private:
// WiFi scan related
std::vector<wifi_ap_record_t> m_ap_records;
bool m_scan_in_progress = false;
// When true, scan results are stored in m_ap_records on scan completion.
// Cleared during connect-to-AP so that the connect-time scan does not
// overwrite the cache with results gathered for connection purposes.
bool m_scan_should_save_ssid = true;
// When true, the next scan-done event responds to a pending GET_WIFI_LIST
// request from the App. Set by the GET_WIFI_LIST handler when no cache is
// available or a scan is already in flight; cleared by the scan-done
// handler after dispatching the response.
bool m_send_list_after_scan = false;
};

View File

@ -110,6 +110,8 @@ void Nt26Board::StartNetwork() {
case UartEthModem::UartEthModemEvent::InFlightMode:
ESP_LOGW(TAG, "Modem in flight mode");
break;
case UartEthModem::UartEthModemEvent::RequestingPdpContext:
break;
}
});

View File

@ -2,4 +2,6 @@
[product](https://store.freenove.com/products/fnk0104)
Official github: [freenove-esp32s3-display-2.8-lcd](https://github.com/Freenove/Freenove_ESP32_S3_Display)
Official github: [freenove-esp32s3-display-2.8-lcd](https://github.com/Freenove/Freenove_ESP32_S3_Display)
Likely the same hardware design as [LCD wiki ES3C28P/ES3N28P](https://www.lcdwiki.com/2.8inch_ESP32-S3_Display)

View File

@ -6,7 +6,7 @@
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\"",
"CONFIG_LANGUAGE_EN_US=y"
"CONFIG_LANGUAGE_EN_US=y",
"CONFIG_SR_WN_WN9S_HIESP=y",
"CONFIG_SR_WN_WN9_HIESP=y"
]

View File

@ -10,6 +10,7 @@
#include "button.h"
#include "config.h"
#include "mcp_server.h"
#include "adc_battery_monitor.h"
#include <esp_log.h>
#include <driver/i2c_master.h>
@ -65,6 +66,11 @@ private:
LcdDisplay *display_;
i2c_master_bus_handle_t codec_i2c_bus_;
TouchDriver touch_;
AdcBatteryMonitor* adc_battery_monitor_;
void InitializeBatteryMonitor() {
adc_battery_monitor_ = new AdcBatteryMonitor(ADC_UNIT_1, ADC_CHANNEL_8, 200000, 200000, GPIO_NUM_NC);
}
static void TouchTask(void *arg) {
auto *self = static_cast<FreenoveESP32S3Display*>(arg);
@ -197,6 +203,7 @@ public:
FreenoveESP32S3Display(): boot_button_(BOOT_BUTTON_GPIO)
{
InitializeI2c();
InitializeBatteryMonitor();
InitializeSpi();
InitializeLcdDisplay();
InitializeTouch();
@ -224,6 +231,13 @@ public:
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight;
}
virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override {
charging = adc_battery_monitor_->IsCharging();
discharging = adc_battery_monitor_->IsDischarging();
level = adc_battery_monitor_->GetBatteryLevel();
return true;
}
};
DECLARE_BOARD(FreenoveESP32S3Display);

View File

@ -8,27 +8,9 @@
## 基础使用
* idf version: v6.0-dev
* idf version: v5.5.2 or above (recommended: v6.0-dev)
1. 调整 idf_component.yml
```yaml
espressif/esp_video:
version: ==1.3.1 # for compatibility. update version may need to modify this project code.
rules:
- if: target not in [esp32]
```
修改为
```yaml
espressif/esp_video:
version: '==0.7.0'
rules:
- if: target not in [esp32]
espressif/esp_ipa: '==0.1.0'
```
* idf version: v5.5.3
* No dependency override needed — the project already specifies the correct `esp_video` and `esp_ipa` versions in `main/idf_component.yml`. Do NOT change the dependency versions unless you are also modifying the source code to match the older API.
针对 ESP32-P4 Rev <3.0 用户:
确保你的 sdkconfig.defaults 包含:
@ -37,7 +19,7 @@ CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y
否则烧写的时候会出现'bootloader/bootloader.bin' requires chip revision in range [v3.0 - v3.99] (this chip is revision v1.x)
2. 使用 release.py 编译
1. 使用 release.py 编译
```shell
python ./scripts/release.py m5stack-tab5
@ -45,7 +27,7 @@ python ./scripts/release.py m5stack-tab5
如需手动编译请参考 `m5stack-tab5/config.json` 修改 menuconfig 对应选项
3. 编译烧录程序
2. 编译烧录程序
```shell
idf.py flash monitor
@ -62,5 +44,3 @@ idf.py flash monitor
1. listening... 需要等几秒才能获取语音输入???
2. 亮度调节不对
3. 音量调节不对
## TODO

View File

@ -205,3 +205,212 @@ otto 机器人具有丰富的动作能力,包括行走、转向、跳跃、摇
**说明**: 小智控制机器人动作是创建新的任务在后台控制,动作执行期间仍可接受新的语音指令。可以通过"停止"语音指令立即停下Otto。
---
## WebSocket 直连调试接口
Otto 机器人内置 WebSocket 服务器,可在局域网内直接调试,无需经过云端。
**连接地址:** `ws://<设备IP>:8080/ws`
> 协议格式JSON-RPC 2.0`id` 字段自行递增即可。
### 连接方式
1. 确认 Otto 已连上 WiFi获取 IP 地址(可通过小程序或串口日志查看)
2. 打开任意 WebSocket 调试工具(如 [websocket.org/echo](https://websocket.org/echo) 或浏览器控制台)
3. 连接 `ws://192.168.x.x:8080/ws`(注意末尾必须有 `/ws`
4. 发送 JSON 命令,响应会直接返回到同一连接
---
### 一、协议初始化(首次连接建议先发)
```json
{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{}},"id":1}
```
---
### 二、获取工具列表
```json
{"jsonrpc":"2.0","method":"tools/list","params":{},"id":2}
```
---
### 三、Otto 机器人工具命令
#### 获取舵机微调值
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.get_trims","arguments":{}},"id":3}
```
#### 设置单个舵机微调(永久保存)
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.set_trim","arguments":{"servo_type":"left_leg","trim_value":5}},"id":4}
```
`servo_type` 可选值:`left_leg` / `right_leg` / `left_foot` / `right_foot` / `left_hand` / `right_hand``trim_value` 范围 `-50` ~ `50`
#### 行走前进3步
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.action","arguments":{"action":"walk","steps":3,"speed":700,"direction":1}},"id":5}
```
#### 后退
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.action","arguments":{"action":"walk","steps":3,"speed":700,"direction":-1}},"id":6}
```
#### 左转
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.action","arguments":{"action":"turn","steps":3,"speed":700,"direction":-1}},"id":7}
```
#### 跳跃
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.action","arguments":{"action":"jump","steps":1,"speed":500}},"id":8}
```
#### 摇摆
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.action","arguments":{"action":"swing","steps":5,"speed":600,"amount":30}},"id":9}
```
#### 太空步
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.action","arguments":{"action":"moonwalk","steps":3,"speed":800,"direction":1,"amount":30}},"id":10}
```
#### 坐下
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.action","arguments":{"action":"sit"}},"id":11}
```
#### 复位
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.action","arguments":{"action":"home"}},"id":12}
```
#### 展示动作
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.action","arguments":{"action":"showcase"}},"id":13}
```
#### 举手(需手部舵机)
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.action","arguments":{"action":"hands_up","speed":500,"direction":1}},"id":14}
```
#### 挥手(需手部舵机)
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.action","arguments":{"action":"hand_wave","direction":1}},"id":15}
```
#### 立即停止所有动作
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.stop","arguments":{}},"id":16}
```
#### 获取运动状态(返回 `"moving"` 或 `"idle"`
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.get_status","arguments":{}},"id":17}
```
#### 获取 IP 地址
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.get_ip","arguments":{}},"id":18}
```
#### 获取电池电量
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.battery.get_level","arguments":{}},"id":19}
```
---
### 四、系统通用工具
#### 获取设备状态(音量/网络/电池等)
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.get_device_status","arguments":{}},"id":20}
```
#### 设置音量0~100
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.audio_speaker.set_volume","arguments":{"volume":70}},"id":21}
```
#### 重启设备
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.reboot","arguments":{}},"id":22}
```
---
### 五、自定义舵机序列
#### 普通移动模式(逐步移动各舵机)
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.servo_sequences","arguments":{"sequence":"{\"a\":[{\"s\":{\"ll\":110,\"rl\":70},\"v\":800},{\"s\":{\"ll\":90,\"rl\":90},\"v\":800}],\"d\":0}"}},"id":23}
```
#### 振荡器模式(双臂摆动)
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.servo_sequences","arguments":{"sequence":"{\"a\":[{\"osc\":{\"a\":{\"lh\":30,\"rh\":30},\"o\":{\"lh\":90,\"rh\":90},\"ph\":{\"rh\":180},\"p\":500,\"c\":5.0}}]}"}},"id":24}
```
#### 振荡器模式(左右摇摆波浪)
```json
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"self.otto.servo_sequences","arguments":{"sequence":"{\"a\":[{\"osc\":{\"a\":{\"ll\":20,\"rl\":20},\"o\":{\"ll\":90,\"rl\":90},\"ph\":{\"rl\":180},\"p\":600,\"c\":5.0}}]}"}},"id":25}
```
**序列舵机键名说明:**
| 键名 | 舵机 | 说明 |
|------|------|------|
| `ll` | 左腿 | 0=完全外展90=中立180=完全内收 |
| `rl` | 右腿 | 0=完全内收90=中立180=完全外展 |
| `lf` | 左脚 | 0=完全向上90=水平180=完全向下 |
| `rf` | 右脚 | 0=完全向下90=水平180=完全向上 |
| `lh` | 左手 | 0=完全向下90=水平180=完全向上 |
| `rh` | 右手 | 0=完全向上90=水平180=完全向下 |
---
### 六、动作参数速查
| 参数 | 说明 | 范围 | 默认 |
|------|------|------|------|
| `steps` | 动作步数 | 1~100 | 3 |
| `speed` | 速度(毫秒,越小越快) | 100~3000 | 700 |
| `direction` | 方向1=前/左,-1=后/右) | -1~1 | 1 |
| `amount` | 幅度 | 0~170 | 30 |
| `arm_swing` | 手臂摆动幅度 | 0~170 | 50 |
| `trim_value` | 舵机微调 | -50~50 | 0 |

View File

@ -230,7 +230,16 @@ private:
if (!ws_control_server_->Start(8080)) {
delete ws_control_server_;
ws_control_server_ = nullptr;
return;
}
// 将 MCP 响应同时广播回连接到 8080 端口的 WebSocket 客户端
Application::GetInstance().RegisterMcpBroadcastCallback(
[this](const std::string& payload) {
if (ws_control_server_) {
ws_control_server_->BroadcastMessage(payload);
}
}
);
}
void StartNetwork() override {

View File

@ -190,3 +190,61 @@ void WebSocketControlServer::RemoveClient(httpd_req_t *req) {
size_t WebSocketControlServer::GetClientCount() const {
return clients_.size();
}
struct WsBroadcastJob {
httpd_handle_t server;
int fd;
char* payload;
size_t len;
};
static void ws_broadcast_send_job(void* arg) {
WsBroadcastJob* job = static_cast<WsBroadcastJob*>(arg);
httpd_ws_frame_t ws_pkt = {};
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
ws_pkt.payload = reinterpret_cast<uint8_t*>(job->payload);
ws_pkt.len = job->len;
ws_pkt.final = true;
esp_err_t ret = httpd_ws_send_frame_async(job->server, job->fd, &ws_pkt);
if (ret != ESP_OK) {
ESP_LOGE("WSControl", "BroadcastMessage: send failed fd=%d err=%d", job->fd, ret);
}
free(job->payload);
free(job);
}
void WebSocketControlServer::BroadcastMessage(const std::string& message) {
if (!server_handle_ || clients_.empty()) {
return;
}
for (auto& [fd, req] : clients_) {
WsBroadcastJob* job = static_cast<WsBroadcastJob*>(malloc(sizeof(WsBroadcastJob)));
if (!job) {
ESP_LOGE(TAG, "BroadcastMessage: failed to allocate job");
continue;
}
job->server = server_handle_;
job->fd = fd;
job->len = message.length();
job->payload = static_cast<char*>(malloc(message.length() + 1));
if (!job->payload) {
ESP_LOGE(TAG, "BroadcastMessage: failed to allocate payload");
free(job);
continue;
}
memcpy(job->payload, message.c_str(), message.length());
job->payload[message.length()] = '\0';
esp_err_t ret = httpd_queue_work(server_handle_, ws_broadcast_send_job, job);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "BroadcastMessage: httpd_queue_work failed fd=%d err=%d", fd, ret);
free(job->payload);
free(job);
}
}
}

View File

@ -17,6 +17,8 @@ public:
size_t GetClientCount() const;
void BroadcastMessage(const std::string& message);
private:
httpd_handle_t server_handle_;
std::map<int, httpd_req_t*> clients_;

View File

@ -0,0 +1,31 @@
# RYMCU BigSmart
该目录为 `RYMCU BigSmart` 开发板适配,并按以下硬件资源完成映射:
- 主控ESP32-S3-WROOM-1-N16R8
- 显示ST7789320x240SPI
- 触摸GT911I2C
- 音频ES8311 + ES7210I2S + I2C
- IO扩展PCA9557I2C 地址 `0x19`
- 摄像头GC0308DVP
参考硬件文档:
- https://github.com/rymcu/BigSmart-Open/blob/main/docs/rymcu-bigsmart-hardware.md
## 编译
```bash
idf.py set-target esp32s3
idf.py menuconfig
```
在菜单中选择:
`Xiaozhi Assistant -> Board Type -> RYMCU BigSmart`
然后执行:
```bash
idf.py build
```

View File

@ -0,0 +1,69 @@
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
#define AUDIO_INPUT_SAMPLE_RATE 24000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
#define AUDIO_INPUT_REFERENCE true
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_38
#define AUDIO_I2S_GPIO_WS GPIO_NUM_13
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_14
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_12
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_45
#define AUDIO_CODEC_USE_PCA9557
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_1
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_2
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
#define AUDIO_CODEC_ES7210_ADDR 0x82
#define BUILTIN_LED_GPIO GPIO_NUM_NC
#define RGB_LED_GPIO GPIO_NUM_43
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define CUSTOM_BUTTON_GPIO GPIO_NUM_10
#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC
#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
#define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY true
#define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_42
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true
#define TOUCH_INT_GPIO GPIO_NUM_NC
#define TOUCH_RST_GPIO GPIO_NUM_NC
/* Camera pins */
#define CAMERA_PIN_PWDN GPIO_NUM_NC
#define CAMERA_PIN_RESET GPIO_NUM_NC
#define CAMERA_PIN_XCLK GPIO_NUM_5
#define CAMERA_PIN_SIOD GPIO_NUM_1
#define CAMERA_PIN_SIOC GPIO_NUM_2
#define CAMERA_PIN_D7 GPIO_NUM_9
#define CAMERA_PIN_D6 GPIO_NUM_4
#define CAMERA_PIN_D5 GPIO_NUM_6
#define CAMERA_PIN_D4 GPIO_NUM_15
#define CAMERA_PIN_D3 GPIO_NUM_17
#define CAMERA_PIN_D2 GPIO_NUM_8
#define CAMERA_PIN_D1 GPIO_NUM_18
#define CAMERA_PIN_D0 GPIO_NUM_16
#define CAMERA_PIN_VSYNC GPIO_NUM_44
#define CAMERA_PIN_HREF GPIO_NUM_46
#define CAMERA_PIN_PCLK GPIO_NUM_7
#define XCLK_FREQ_HZ 16000000
#define BATTERY_CHARGING_PIN GPIO_NUM_3
#define BATTERY_ADC_PIN GPIO_NUM_11
#endif // _BOARD_CONFIG_H_

View File

@ -0,0 +1,12 @@
{
"manufacturer": "rymcu",
"target": "esp32s3",
"builds": [
{
"name": "bigsmart",
"sdkconfig_append": [
"CONFIG_USE_DEVICE_AEC=y"
]
}
]
}

View File

@ -0,0 +1,284 @@
#include "wifi_board.h"
#include "codecs/box_audio_codec.h"
#include "display/lcd_display.h"
#include "display/emote_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "i2c_device.h"
#include "esp32_camera.h"
#include "mcp_server.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <driver/i2c_master.h>
#include <driver/spi_common.h>
#include <esp_lcd_touch_gt911.h>
#include <esp_lvgl_port.h>
#include <lvgl.h>
#define TAG "RymcuBigsmartBoard"
class Pca9557 : public I2cDevice {
public:
Pca9557(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
WriteReg(0x01, 0x03);
WriteReg(0x03, 0xf8);
}
void SetOutputState(uint8_t bit, uint8_t level) {
uint8_t data = ReadReg(0x01);
data = (data & ~(1 << bit)) | (level << bit);
WriteReg(0x01, data);
}
};
class CustomAudioCodec : public BoxAudioCodec {
private:
Pca9557* pca9557_;
public:
CustomAudioCodec(i2c_master_bus_handle_t i2c_bus, Pca9557* pca9557)
: BoxAudioCodec(i2c_bus,
AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK,
AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN,
GPIO_NUM_NC,
AUDIO_CODEC_ES8311_ADDR,
AUDIO_CODEC_ES7210_ADDR,
AUDIO_INPUT_REFERENCE),
pca9557_(pca9557) {
}
virtual void EnableOutput(bool enable) override {
BoxAudioCodec::EnableOutput(enable);
if (enable) {
pca9557_->SetOutputState(1, 1);
} else {
pca9557_->SetOutputState(1, 0);
}
}
};
class RymcuBigsmartBoard : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
Button boot_button_;
Display* display_;
Pca9557* pca9557_;
Esp32Camera* camera_;
void InitializeI2c() {
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = (i2c_port_t)1,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
pca9557_ = new Pca9557(i2c_bus_, 0x19);
}
void InitializeSpi() {
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = GPIO_NUM_40;
buscfg.miso_io_num = GPIO_NUM_NC;
buscfg.sclk_io_num = GPIO_NUM_41;
buscfg.quadwp_io_num = GPIO_NUM_NC;
buscfg.quadhd_io_num = GPIO_NUM_NC;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});
#if CONFIG_USE_DEVICE_AEC
boot_button_.OnDoubleClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateIdle) {
app.SetAecMode(app.GetAecMode() == kAecOff ? kAecOnDeviceSide : kAecOff);
}
});
#endif
}
void InitializeSt7789Display() {
esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr;
ESP_LOGD(TAG, "Install panel IO");
esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = GPIO_NUM_NC;
io_config.dc_gpio_num = GPIO_NUM_39;
io_config.spi_mode = 2;
io_config.pclk_hz = 80 * 1000 * 1000;
io_config.trans_queue_depth = 10;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io));
ESP_LOGD(TAG, "Install LCD driver");
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = GPIO_NUM_NC;
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
panel_config.bits_per_pixel = 16;
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel);
pca9557_->SetOutputState(0, 0);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, true);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
esp_lcd_panel_disp_on_off(panel, true);
#if CONFIG_USE_EMOTE_MESSAGE_STYLE
display_ = new emote::EmoteDisplay(panel, panel_io, DISPLAY_WIDTH, DISPLAY_HEIGHT);
#else
display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
#endif
}
void InitializeTouch() {
esp_lcd_touch_handle_t tp;
esp_lcd_touch_config_t tp_cfg = {
.x_max = DISPLAY_HEIGHT,
.y_max = DISPLAY_WIDTH,
.rst_gpio_num = TOUCH_RST_GPIO,
.int_gpio_num = TOUCH_INT_GPIO,
.levels = {
.reset = 0,
.interrupt = 0,
},
.flags = {
.swap_xy = 1,
.mirror_x = 1,
.mirror_y = 0,
},
};
esp_lcd_panel_io_handle_t tp_io_handle = NULL;
esp_lcd_panel_io_i2c_config_t tp_io_config = {
.dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS,
.control_phase_bytes = 1,
.dc_bit_offset = 0,
.lcd_cmd_bits = 16,
.flags = {
.disable_control_phase = 1,
}
};
tp_io_config.scl_speed_hz = 400000;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus_, &tp_io_config, &tp_io_handle));
ESP_ERROR_CHECK(esp_lcd_touch_new_i2c_gt911(tp_io_handle, &tp_cfg, &tp));
assert(tp);
const lvgl_port_touch_cfg_t touch_cfg = {
.disp = lv_display_get_default(),
.handle = tp,
};
if (touch_cfg.disp) {
lvgl_port_add_touch(&touch_cfg);
} else {
ESP_LOGE(TAG, "Touch display is not initialized");
}
}
void InitializeCamera() {
pca9557_->SetOutputState(2, 0);
camera_config_t config = {};
config.ledc_channel = LEDC_CHANNEL_2;
config.ledc_timer = LEDC_TIMER_2;
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 = -1;
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;
camera_ = new Esp32Camera(config);
}
void InitializeTools() {
auto& mcp_server = McpServer::GetInstance();
mcp_server.AddTool("self.system.reconfigure_wifi",
"End this conversation and enter WiFi configuration mode.\n"
"**CAUTION** You must ask the user to confirm this action.",
PropertyList(), [this](const PropertyList& properties) {
EnterWifiConfigMode();
return true;
});
}
public:
RymcuBigsmartBoard() : boot_button_(BOOT_BUTTON_GPIO) {
InitializeI2c();
InitializeSpi();
InitializeSt7789Display();
InitializeTouch();
InitializeButtons();
InitializeCamera();
InitializeTools();
GetBacklight()->RestoreBrightness();
}
virtual AudioCodec* GetAudioCodec() override {
static CustomAudioCodec audio_codec(i2c_bus_, pca9557_);
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
virtual Backlight* GetBacklight() override {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight;
}
virtual Camera* GetCamera() override {
return camera_;
}
};
DECLARE_BOARD(RymcuBigsmartBoard);

View File

@ -20,12 +20,12 @@ dependencies:
waveshare/custom_io_expander_ch32v003: ^1.0.0
espressif/esp_lcd_panel_io_additions: ^1.0.1
78/esp_lcd_nv3023: ~1.0.0
78/esp-wifi-connect: ~3.1.2
78/esp-wifi-connect: ~3.1.3
espressif/esp_audio_effects: ~1.2.1
espressif/esp_audio_codec: ~2.4.1
78/esp-ml307: ~3.6.5
78/uart-eth-modem:
version: ~0.3.4
version: ~0.4.0
rules:
- if: target not in [esp32]
78/xiaozhi-fonts: ~1.6.0

View File

@ -284,10 +284,8 @@ void McpServer::AddUserOnlyTools() {
}
#endif // HAVE_LVGL
// Assets download url
auto& assets = Assets::GetInstance();
if (assets.partition_valid()) {
AddUserOnlyTool("self.assets.set_download_url", "Set the download url for the assets",
// Assets download url (always registered — Settings storage works regardless of partition layout)
AddUserOnlyTool("self.assets.set_download_url", "Set the download url for the assets",
PropertyList({
Property("url", kPropertyTypeString)
}),
@ -297,7 +295,6 @@ void McpServer::AddUserOnlyTools() {
settings.SetString("download_url", url);
return true;
});
}
}
void McpServer::AddTool(McpTool* tool) {