feat: ESP-Hi 改用 MCP 协议 (#740)

* feat(esp-hi): use MCP protocol

* docs(esp-hi): Add README

* feat(esp-hi): remove redundant checks and add option to disable WebUI
This commit is contained in:
laride
2025-06-03 16:45:03 +08:00
committed by GitHub
parent 9122664f0f
commit bfc695b1ee
6 changed files with 208 additions and 258 deletions

View File

@@ -0,0 +1,51 @@
# ESP-Hi
## 简介
<div align="center">
<a href="https://oshwhub.com/esp-college/esp-hi"><b> 立创开源平台 </b></a>
|
<a href="https://www.bilibili.com/video/BV1BHJtz6E2S"><b> Bilibili </b></a>
</div>
ESP-Hi 是 ESP Friends 开源的一款基于 ESP32C3 的超**低成本** AI 对话机器人。ESP-Hi 集成了一个0.96寸的彩屏,用于显示表情,**机器狗已实现数十种动作**。通过对 ESP32-C3 外设的充分挖掘,仅需最少的板级硬件即可实现拾音和发声,同步优化了软件,降低内存与 Flash 占用,在资源受限的情况下同时实现了**唤醒词检测**与多种外设驱动。硬件详情等可查看[立创开源项目](https://oshwhub.com/esp-college/esp-hi)。
## WebUI
ESP-Hi x 小智内置了一个控制身体运动的 WebUI请将手机与 ESP-Hi 连接到同一个 Wi-Fi 下,手机访问 `http://esp-hi.local/` 以使用。
如需禁用,请取消 `ESP_HI_WEB_CONTROL_ENABLED`,即取消勾选 `Component config``Servo Dog Configuration``Web Control``Enable ESP-HI Web Control`
## 配置、编译命令
由于 ESP-Hi 需要配置较多的 sdkconfig 选项,推荐使用编译脚本编译。
**编译**
```bash
python ./scripts/release.py esp-hi
```
如需手动编译,请参考 `esp-hi/config.json` 修改 menuconfig 对应选项。
**烧录**
```bash
idf.py flash
```
> [!TIP]
>
> **舵机控制会占用 ESP-Hi 的 USB Type-C 接口**,导致无法连接电脑(无法烧录/查看运行日志)。如遇此情况,请按以下提示操作:
>
> **烧录**
>
> 1. 断开 ESP-Hi 的电源,只留头部,不要连接身体。
> 2. 按住 ESP-Hi 的按钮并连接电脑。
>
> 此时ESP-Hi (ESP32C3) 应当处于烧录模式,可以使用电脑烧录程序。烧录完成后,可能需要重新插拔电源。
>
> **查看 log**
>
> 请设置 `CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y`,即 `Component config` → `ESP System Settings` → `Channel for console output` 选择 `USB Serial/JTAG Controller`。这同时会禁用舵机控制功能。

View File

@@ -31,7 +31,7 @@
"CONFIG_NEWLIB_NANO_FORMAT=y", "CONFIG_NEWLIB_NANO_FORMAT=y",
"CONFIG_MMAP_FILE_NAME_LENGTH=25", "CONFIG_MMAP_FILE_NAME_LENGTH=25",
"CONFIG_ESP_CONSOLE_NONE=y", "CONFIG_ESP_CONSOLE_NONE=y",
"CONFIG_IOT_PROTOCOL_XIAOZHI=y" "CONFIG_IOT_PROTOCOL_MCP=y"
] ]
} }
] ]

View File

@@ -1,64 +0,0 @@
#include "iot/thing.h"
#include "board.h"
#include "audio_codec.h"
#include <driver/gpio.h>
#include <esp_log.h>
#include "servo_dog_ctrl.h"
#define TAG "Message"
namespace iot {
class DogAction_extra : public Thing {
private:
bool is_moving_ = false;
void InitializePlayer()
{
ESP_LOGI(TAG, "Dog action initialized");
}
public:
DogAction_extra() : Thing("DogAction_extra", "机器人扩展动作控制")
{
InitializePlayer();
// 定义设备的属性
properties_.AddBooleanProperty("is_moving", "机器人是否正在移动", [this]() -> bool {
return is_moving_;
});
// 定义设备可以被远程执行的指令
methods_.AddMethod("retract_legs", "机器人收回腿部", ParameterList(), [this](const ParameterList & parameters) {
is_moving_ = true;
servo_dog_ctrl_send(DOG_STATE_RETRACT_LEGS, NULL);
});
methods_.AddMethod("stop", "立即停止机器人当前动作", ParameterList(), [this](const ParameterList & parameters) {
if (is_moving_) {
is_moving_ = false;
servo_dog_ctrl_send(DOG_STATE_IDLE, NULL);
}
});
methods_.AddMethod("shake_hand", "机器人做握手动作", ParameterList(), [this](const ParameterList & parameters) {
is_moving_ = true;
servo_dog_ctrl_send(DOG_STATE_SHAKE_HAND, NULL);
});
methods_.AddMethod("shake_back_legs", "机器人伸懒腰", ParameterList(), [this](const ParameterList & parameters) {
is_moving_ = true;
servo_dog_ctrl_send(DOG_STATE_SHAKE_BACK_LEGS, NULL);
});
methods_.AddMethod("jump_forward", "机器人向前跳跃", ParameterList(), [this](const ParameterList & parameters) {
is_moving_ = true;
servo_dog_ctrl_send(DOG_STATE_JUMP_FORWARD, NULL);
});
}
};
} // namespace iot
DECLARE_THING(DogAction_extra);

View File

@@ -1,75 +0,0 @@
#include "iot/thing.h"
#include "board.h"
#include "audio_codec.h"
#include <driver/gpio.h>
#include <esp_log.h>
#include "servo_dog_ctrl.h"
#define TAG "Message"
namespace iot {
class DogAction_basic : public Thing {
private:
bool is_moving_ = false;
void InitializePlayer()
{
ESP_LOGI(TAG, "Dog action initialized");
}
public:
DogAction_basic() : Thing("DogAction_basic", "机器人基础动作控制")
{
InitializePlayer();
// 定义设备的属性
properties_.AddBooleanProperty("is_moving", "机器人是否正在移动", [this]() -> bool {
return is_moving_;
});
// 定义设备可以被远程执行的指令
methods_.AddMethod("forward", "机器人向前移动", ParameterList(), [this](const ParameterList & parameters) {
is_moving_ = true;
servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL);
});
methods_.AddMethod("backward", "机器人向后移动", ParameterList(), [this](const ParameterList & parameters) {
is_moving_ = true;
servo_dog_ctrl_send(DOG_STATE_BACKWARD, NULL);
});
methods_.AddMethod("sway_back_forth", "机器人做前后摇摆动作", ParameterList(), [this](const ParameterList & parameters) {
is_moving_ = true;
servo_dog_ctrl_send(DOG_STATE_SWAY_BACK_FORTH, NULL);
});
methods_.AddMethod("turn_left", "机器人向左转", ParameterList(), [this](const ParameterList & parameters) {
is_moving_ = true;
servo_dog_ctrl_send(DOG_STATE_TURN_LEFT, NULL);
});
methods_.AddMethod("turn_right", "机器人向右转", ParameterList(), [this](const ParameterList & parameters) {
is_moving_ = true;
servo_dog_ctrl_send(DOG_STATE_TURN_RIGHT, NULL);
});
methods_.AddMethod("lay_down", "机器人趴下", ParameterList(), [this](const ParameterList & parameters) {
is_moving_ = true;
servo_dog_ctrl_send(DOG_STATE_LAY_DOWN, NULL);
});
methods_.AddMethod("sway", "机器人做左右摇摆动作", ParameterList(), [this](const ParameterList & parameters) {
is_moving_ = true;
dog_action_args_t args = {
.repeat_count = 4,
};
servo_dog_ctrl_send(DOG_STATE_SWAY, &args);
});
}
};
} // namespace iot
DECLARE_THING(DogAction_basic);

View File

@@ -1,113 +0,0 @@
#include "iot/thing.h"
#include "board.h"
#include "audio_codec.h"
#include <driver/gpio.h>
#include <esp_log.h>
#include "driver/rmt_tx.h"
#include "led_strip.h"
#define TAG "Light"
static led_strip_handle_t led_strip;
static const led_strip_config_t bsp_strip_config = {
.strip_gpio_num = GPIO_NUM_8,
.max_leds = 4,
.led_model = LED_MODEL_WS2812,
.flags = {
.invert_out = false
}
};
static const led_strip_rmt_config_t bsp_rmt_config = {
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = 10 * 1000 * 1000,
.flags = {
.with_dma = false
}
};
esp_err_t bsp_led_init()
{
ESP_LOGI(TAG, "BLINK_GPIO setting %d", bsp_strip_config.strip_gpio_num);
ESP_ERROR_CHECK(led_strip_new_rmt_device(&bsp_strip_config, &bsp_rmt_config, &led_strip));
led_strip_set_pixel(led_strip, 0, 0x00, 0x00, 0x00);
led_strip_set_pixel(led_strip, 1, 0x00, 0x00, 0x00);
led_strip_set_pixel(led_strip, 2, 0x00, 0x00, 0x00);
led_strip_set_pixel(led_strip, 3, 0x00, 0x00, 0x00);
led_strip_refresh(led_strip);
return ESP_OK;
}
esp_err_t bsp_led_rgb_set(uint8_t r, uint8_t g, uint8_t b)
{
esp_err_t ret = ESP_OK;
ret |= led_strip_set_pixel(led_strip, 0, r, g, b);
ret |= led_strip_set_pixel(led_strip, 1, r, g, b);
ret |= led_strip_set_pixel(led_strip, 2, r, g, b);
ret |= led_strip_set_pixel(led_strip, 3, r, g, b);
ret |= led_strip_refresh(led_strip);
return ret;
}
namespace iot {
class DogLight : public Thing {
private:
bool power_ = false;
void InitializeGpio()
{
bsp_led_init();
bsp_led_rgb_set(0x00, 0x00, 0x00);
ESP_LOGI(TAG, "lamp InitializeGpio");
}
public:
DogLight() : Thing("DogLight", "机器人头灯"), power_(false)
{
InitializeGpio();
properties_.AddBooleanProperty("power", "灯是否打开", [this]() -> bool {
return power_;
});
methods_.AddMethod("TurnOn", "打开灯", ParameterList(), [this](const ParameterList & parameters) {
power_ = true;
bsp_led_rgb_set(0xFF, 0xFF, 0xFF);
ESP_LOGI(TAG, "lamp TurnOn");
});
methods_.AddMethod("TurnOff", "关闭灯", ParameterList(), [this](const ParameterList & parameters) {
power_ = false;
bsp_led_rgb_set(0x00, 0x00, 0x00);
ESP_LOGI(TAG, "lamp TurnOff");
});
methods_.AddMethod("SetRGB", "设置RGB颜色",
ParameterList({
Parameter("r", "红色值(0-255)", kValueTypeNumber, true),
Parameter("g", "绿色值(0-255)", kValueTypeNumber, true),
Parameter("b", "蓝色值(0-255)", kValueTypeNumber, true)
}), [this](const ParameterList & parameters) {
int r = parameters["r"].number();
int g = parameters["g"].number();
int b = parameters["b"].number();
r = std::max(0, std::min(255, r));
g = std::max(0, std::min(255, g));
b = std::max(0, std::min(255, b));
power_ = true;
bsp_led_rgb_set(r, g, b);
ESP_LOGI(TAG, "lamp SetRGB: r=%d, g=%d, b=%d", r, g, b);
});
}
};
} // namespace iot
DECLARE_THING(DogLight);

View File

@@ -3,7 +3,7 @@
#include "application.h" #include "application.h"
#include "button.h" #include "button.h"
#include "config.h" #include "config.h"
#include "iot/thing_manager.h" #include "mcp_server.h"
#include <wifi_station.h> #include <wifi_station.h>
#include <esp_log.h> #include <esp_log.h>
#include <driver/i2c_master.h> #include <driver/i2c_master.h>
@@ -21,8 +21,14 @@
#include "anim_player.h" #include "anim_player.h"
#include "emoji_display.h" #include "emoji_display.h"
#include "servo_dog_ctrl.h" #include "servo_dog_ctrl.h"
#include "led_strip.h"
#include "driver/rmt_tx.h"
#include "sdkconfig.h"
#ifdef CONFIG_ESP_HI_WEB_CONTROL_ENABLED
#include "esp_hi_web_control.h" #include "esp_hi_web_control.h"
#endif //CONFIG_ESP_HI_WEB_CONTROL_ENABLED
#define TAG "ESP_HI" #define TAG "ESP_HI"
@@ -47,6 +53,23 @@ static const ili9341_lcd_init_cmd_t vendor_specific_init[] = {
{0x2C, NULL, 0, 0}, // Memory write {0x2C, NULL, 0, 0}, // Memory write
}; };
static const led_strip_config_t bsp_strip_config = {
.strip_gpio_num = GPIO_NUM_8,
.max_leds = 4,
.led_model = LED_MODEL_WS2812,
.flags = {
.invert_out = false
}
};
static const led_strip_rmt_config_t bsp_rmt_config = {
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = 10 * 1000 * 1000,
.flags = {
.with_dma = false
}
};
class EspHi : public WifiBoard { class EspHi : public WifiBoard {
private: private:
Button boot_button_; Button boot_button_;
@@ -54,7 +77,10 @@ private:
Button move_wake_button_; Button move_wake_button_;
anim::EmojiWidget* display_ = nullptr; anim::EmojiWidget* display_ = nullptr;
bool web_server_initialized_ = false; bool web_server_initialized_ = false;
led_strip_handle_t led_strip_;
bool led_on_ = false;
#ifdef CONFIG_ESP_HI_WEB_CONTROL_ENABLED
static void wifi_event_handler(void* arg, esp_event_base_t event_base, static void wifi_event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data) int32_t event_id, void* event_data)
{ {
@@ -72,6 +98,7 @@ private:
} }
} }
} }
#endif //CONFIG_ESP_HI_WEB_CONTROL_ENABLED
void HandleMoveWakePressDown(int64_t current_time, int64_t &last_trigger_time, int &gesture_state) void HandleMoveWakePressDown(int64_t current_time, int64_t &last_trigger_time, int &gesture_state)
{ {
@@ -154,17 +181,39 @@ private:
HandleMoveWakePressUp(current_time, last_trigger_time, gesture_state); HandleMoveWakePressUp(current_time, last_trigger_time, gesture_state);
}); });
} }
void InitializeLed() {
ESP_LOGI(TAG, "BLINK_GPIO setting %d", bsp_strip_config.strip_gpio_num);
ESP_ERROR_CHECK(led_strip_new_rmt_device(&bsp_strip_config, &bsp_rmt_config, &led_strip_));
led_strip_set_pixel(led_strip_, 0, 0x00, 0x00, 0x00);
led_strip_set_pixel(led_strip_, 1, 0x00, 0x00, 0x00);
led_strip_set_pixel(led_strip_, 2, 0x00, 0x00, 0x00);
led_strip_set_pixel(led_strip_, 3, 0x00, 0x00, 0x00);
led_strip_refresh(led_strip_);
}
esp_err_t SetLedColor(uint8_t r, uint8_t g, uint8_t b) {
esp_err_t ret = ESP_OK;
ret |= led_strip_set_pixel(led_strip_, 0, r, g, b);
ret |= led_strip_set_pixel(led_strip_, 1, r, g, b);
ret |= led_strip_set_pixel(led_strip_, 2, r, g, b);
ret |= led_strip_set_pixel(led_strip_, 3, r, g, b);
ret |= led_strip_refresh(led_strip_);
return ret;
}
void InitializeIot() void InitializeIot()
{ {
ESP_LOGI(TAG, "Initialize Iot"); ESP_LOGI(TAG, "Initialize Iot");
auto &thing_manager = iot::ThingManager::GetInstance(); InitializeLed();
thing_manager.AddThing(iot::CreateThing("DogLight")); SetLedColor(0x00, 0x00, 0x00);
thing_manager.AddThing(iot::CreateThing("DogAction_basic"));
thing_manager.AddThing(iot::CreateThing("DogAction_extra"));
#ifdef CONFIG_ESP_HI_WEB_CONTROL_ENABLED
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED,
&wifi_event_handler, this)); &wifi_event_handler, this));
#endif //CONFIG_ESP_HI_WEB_CONTROL_ENABLED
} }
void InitializeSpi() void InitializeSpi()
@@ -235,6 +284,107 @@ private:
#endif #endif
} }
void InitializeTools()
{
auto& mcp_server = McpServer::GetInstance();
// 基础动作控制
mcp_server.AddTool("self.dog.forward", "机器人向前移动", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL);
return true;
});
mcp_server.AddTool("self.dog.backward", "机器人向后移动", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_BACKWARD, NULL);
return true;
});
mcp_server.AddTool("self.dog.sway_back_forth", "机器人做前后摇摆动作", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_SWAY_BACK_FORTH, NULL);
return true;
});
mcp_server.AddTool("self.dog.turn_left", "机器人向左转", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_TURN_LEFT, NULL);
return true;
});
mcp_server.AddTool("self.dog.turn_right", "机器人向右转", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_TURN_RIGHT, NULL);
return true;
});
mcp_server.AddTool("self.dog.lay_down", "机器人趴下", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_LAY_DOWN, NULL);
return true;
});
mcp_server.AddTool("self.dog.sway", "机器人做左右摇摆动作", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
dog_action_args_t args = {
.repeat_count = 4,
};
servo_dog_ctrl_send(DOG_STATE_SWAY, &args);
return true;
});
// 扩展动作控制
mcp_server.AddTool("self.dog.retract_legs", "机器人收回腿部", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_RETRACT_LEGS, NULL);
return true;
});
mcp_server.AddTool("self.dog.stop", "立即停止机器人当前动作", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_IDLE, NULL);
return true;
});
mcp_server.AddTool("self.dog.shake_hand", "机器人做握手动作", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_SHAKE_HAND, NULL);
return true;
});
mcp_server.AddTool("self.dog.shake_back_legs", "机器人伸懒腰", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_SHAKE_BACK_LEGS, NULL);
return true;
});
mcp_server.AddTool("self.dog.jump_forward", "机器人向前跳跃", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_JUMP_FORWARD, NULL);
return true;
});
// 灯光控制
mcp_server.AddTool("self.light.get_power", "获取灯是否打开", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
return led_on_;
});
mcp_server.AddTool("self.light.turn_on", "打开灯", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
SetLedColor(0xFF, 0xFF, 0xFF);
led_on_ = true;
return true;
});
mcp_server.AddTool("self.light.turn_off", "关闭灯", PropertyList(), [this](const PropertyList& properties) -> ReturnValue {
SetLedColor(0x00, 0x00, 0x00);
led_on_ = false;
return true;
});
mcp_server.AddTool("self.light.set_rgb", "设置RGB颜色", PropertyList({
Property("r", kPropertyTypeInteger, 0, 255),
Property("g", kPropertyTypeInteger, 0, 255),
Property("b", kPropertyTypeInteger, 0, 255)
}), [this](const PropertyList& properties) -> ReturnValue {
int r = properties["r"].value<int>();
int g = properties["g"].value<int>();
int b = properties["b"].value<int>();
led_on_ = true;
SetLedColor(r, g, b);
return true;
});
}
public: public:
EspHi() : boot_button_(BOOT_BUTTON_GPIO), EspHi() : boot_button_(BOOT_BUTTON_GPIO),
audio_wake_button_(AUDIO_WAKE_BUTTON_GPIO), audio_wake_button_(AUDIO_WAKE_BUTTON_GPIO),
@@ -244,6 +394,7 @@ public:
InitializeIot(); InitializeIot();
InitializeSpi(); InitializeSpi();
InitializeLcdDisplay(); InitializeLcdDisplay();
InitializeTools();
} }
virtual AudioCodec* GetAudioCodec() override virtual AudioCodec* GetAudioCodec() override