mirror of
https://github.com/78/xiaozhi-esp32.git
synced 2026-01-14 01:07:30 +08:00
add update with custom ota url - storage on aws
This commit is contained in:
parent
1b632ebd63
commit
86a0314a87
@ -124,6 +124,263 @@ config USE_ERA_SMART_HOME
|
||||
help
|
||||
Enable E-Ra Smart Home control features.
|
||||
|
||||
menu "ERA Smart Home Devices"
|
||||
depends on USE_ERA_SMART_HOME
|
||||
|
||||
config ERA_DEVICE_COUNT
|
||||
int "Number of Devices"
|
||||
range 0 5
|
||||
default 0
|
||||
help
|
||||
Number of ERA Smart Home devices to configure.
|
||||
|
||||
menu "Device 1"
|
||||
depends on ERA_DEVICE_COUNT >= 1
|
||||
|
||||
config ERA_DEVICE_1_NAME
|
||||
string "Device Name"
|
||||
default "Device 1"
|
||||
help
|
||||
Name of the device (e.g., Living Room Light).
|
||||
|
||||
choice ERA_DEVICE_1_TYPE
|
||||
prompt "Device Type"
|
||||
default ERA_DEVICE_1_TYPE_LIGHT
|
||||
|
||||
config ERA_DEVICE_1_TYPE_SWITCH
|
||||
bool "Switch"
|
||||
config ERA_DEVICE_1_TYPE_LIGHT
|
||||
bool "Light"
|
||||
config ERA_DEVICE_1_TYPE_MOTOR
|
||||
bool "Motor"
|
||||
config ERA_DEVICE_1_TYPE_OTHER
|
||||
bool "Other"
|
||||
endchoice
|
||||
|
||||
config ERA_DEVICE_1_OTHER_TYPE_NAME
|
||||
string "Custom Type Name"
|
||||
depends on ERA_DEVICE_1_TYPE_OTHER
|
||||
default "Device"
|
||||
help
|
||||
Specify the type name if 'Other' is selected.
|
||||
|
||||
config ERA_DEVICE_1_CONFIG_ID
|
||||
string "Config ID"
|
||||
default ""
|
||||
help
|
||||
Config ID for checking device status.
|
||||
|
||||
config ERA_DEVICE_1_ACTION_ON
|
||||
string "Action On Key"
|
||||
default ""
|
||||
help
|
||||
Action key to turn the device ON.
|
||||
|
||||
config ERA_DEVICE_1_ACTION_OFF
|
||||
string "Action Off Key"
|
||||
default ""
|
||||
help
|
||||
Action key to turn the device OFF.
|
||||
endmenu
|
||||
|
||||
menu "Device 2"
|
||||
depends on ERA_DEVICE_COUNT >= 2
|
||||
|
||||
config ERA_DEVICE_2_NAME
|
||||
string "Device Name"
|
||||
default "Device 2"
|
||||
help
|
||||
Name of the device.
|
||||
|
||||
choice ERA_DEVICE_2_TYPE
|
||||
prompt "Device Type"
|
||||
default ERA_DEVICE_2_TYPE_LIGHT
|
||||
|
||||
config ERA_DEVICE_2_TYPE_SWITCH
|
||||
bool "Switch"
|
||||
config ERA_DEVICE_2_TYPE_LIGHT
|
||||
bool "Light"
|
||||
config ERA_DEVICE_2_TYPE_MOTOR
|
||||
bool "Motor"
|
||||
config ERA_DEVICE_2_TYPE_OTHER
|
||||
bool "Other"
|
||||
endchoice
|
||||
|
||||
config ERA_DEVICE_2_OTHER_TYPE_NAME
|
||||
string "Custom Type Name"
|
||||
depends on ERA_DEVICE_2_TYPE_OTHER
|
||||
default "Device"
|
||||
help
|
||||
Specify the type name if 'Other' is selected.
|
||||
|
||||
config ERA_DEVICE_2_CONFIG_ID
|
||||
string "Config ID"
|
||||
default ""
|
||||
help
|
||||
Config ID for checking device status.
|
||||
|
||||
config ERA_DEVICE_2_ACTION_ON
|
||||
string "Action On Key"
|
||||
default ""
|
||||
help
|
||||
Action key to turn the device ON.
|
||||
|
||||
config ERA_DEVICE_2_ACTION_OFF
|
||||
string "Action Off Key"
|
||||
default ""
|
||||
help
|
||||
Action key to turn the device OFF.
|
||||
endmenu
|
||||
|
||||
menu "Device 3"
|
||||
depends on ERA_DEVICE_COUNT >= 3
|
||||
|
||||
config ERA_DEVICE_3_NAME
|
||||
string "Device Name"
|
||||
default "Device 3"
|
||||
help
|
||||
Name of the device.
|
||||
|
||||
choice ERA_DEVICE_3_TYPE
|
||||
prompt "Device Type"
|
||||
default ERA_DEVICE_3_TYPE_LIGHT
|
||||
|
||||
config ERA_DEVICE_3_TYPE_SWITCH
|
||||
bool "Switch"
|
||||
config ERA_DEVICE_3_TYPE_LIGHT
|
||||
bool "Light"
|
||||
config ERA_DEVICE_3_TYPE_MOTOR
|
||||
bool "Motor"
|
||||
config ERA_DEVICE_3_TYPE_OTHER
|
||||
bool "Other"
|
||||
endchoice
|
||||
|
||||
config ERA_DEVICE_3_OTHER_TYPE_NAME
|
||||
string "Custom Type Name"
|
||||
depends on ERA_DEVICE_3_TYPE_OTHER
|
||||
default "Device"
|
||||
help
|
||||
Specify the type name if 'Other' is selected.
|
||||
|
||||
config ERA_DEVICE_3_CONFIG_ID
|
||||
string "Config ID"
|
||||
default ""
|
||||
help
|
||||
Config ID for checking device status.
|
||||
|
||||
config ERA_DEVICE_3_ACTION_ON
|
||||
string "Action On Key"
|
||||
default ""
|
||||
help
|
||||
Action key to turn the device ON.
|
||||
|
||||
config ERA_DEVICE_3_ACTION_OFF
|
||||
string "Action Off Key"
|
||||
default ""
|
||||
help
|
||||
Action key to turn the device OFF.
|
||||
endmenu
|
||||
|
||||
menu "Device 4"
|
||||
depends on ERA_DEVICE_COUNT >= 4
|
||||
|
||||
config ERA_DEVICE_4_NAME
|
||||
string "Device Name"
|
||||
default "Device 4"
|
||||
help
|
||||
Name of the device.
|
||||
|
||||
choice ERA_DEVICE_4_TYPE
|
||||
prompt "Device Type"
|
||||
default ERA_DEVICE_4_TYPE_LIGHT
|
||||
|
||||
config ERA_DEVICE_4_TYPE_SWITCH
|
||||
bool "Switch"
|
||||
config ERA_DEVICE_4_TYPE_LIGHT
|
||||
bool "Light"
|
||||
config ERA_DEVICE_4_TYPE_MOTOR
|
||||
bool "Motor"
|
||||
config ERA_DEVICE_4_TYPE_OTHER
|
||||
bool "Other"
|
||||
endchoice
|
||||
|
||||
config ERA_DEVICE_4_OTHER_TYPE_NAME
|
||||
string "Custom Type Name"
|
||||
depends on ERA_DEVICE_4_TYPE_OTHER
|
||||
default "Device"
|
||||
help
|
||||
Specify the type name if 'Other' is selected.
|
||||
|
||||
config ERA_DEVICE_4_CONFIG_ID
|
||||
string "Config ID"
|
||||
default ""
|
||||
help
|
||||
Config ID for checking device status.
|
||||
|
||||
config ERA_DEVICE_4_ACTION_ON
|
||||
string "Action On Key"
|
||||
default ""
|
||||
help
|
||||
Action key to turn the device ON.
|
||||
|
||||
config ERA_DEVICE_4_ACTION_OFF
|
||||
string "Action Off Key"
|
||||
default ""
|
||||
help
|
||||
Action key to turn the device OFF.
|
||||
endmenu
|
||||
|
||||
menu "Device 5"
|
||||
depends on ERA_DEVICE_COUNT >= 5
|
||||
|
||||
config ERA_DEVICE_5_NAME
|
||||
string "Device Name"
|
||||
default "Device 5"
|
||||
help
|
||||
Name of the device.
|
||||
|
||||
choice ERA_DEVICE_5_TYPE
|
||||
prompt "Device Type"
|
||||
default ERA_DEVICE_5_TYPE_LIGHT
|
||||
|
||||
config ERA_DEVICE_5_TYPE_SWITCH
|
||||
bool "Switch"
|
||||
config ERA_DEVICE_5_TYPE_LIGHT
|
||||
bool "Light"
|
||||
config ERA_DEVICE_5_TYPE_MOTOR
|
||||
bool "Motor"
|
||||
config ERA_DEVICE_5_TYPE_OTHER
|
||||
bool "Other"
|
||||
endchoice
|
||||
|
||||
config ERA_DEVICE_5_OTHER_TYPE_NAME
|
||||
string "Custom Type Name"
|
||||
depends on ERA_DEVICE_5_TYPE_OTHER
|
||||
default "Device"
|
||||
help
|
||||
Specify the type name if 'Other' is selected.
|
||||
|
||||
config ERA_DEVICE_5_CONFIG_ID
|
||||
string "Config ID"
|
||||
default ""
|
||||
help
|
||||
Config ID for checking device status.
|
||||
|
||||
config ERA_DEVICE_5_ACTION_ON
|
||||
string "Action On Key"
|
||||
default ""
|
||||
help
|
||||
Action key to turn the device ON.
|
||||
|
||||
config ERA_DEVICE_5_ACTION_OFF
|
||||
string "Action Off Key"
|
||||
default ""
|
||||
help
|
||||
Action key to turn the device OFF.
|
||||
endmenu
|
||||
|
||||
endmenu
|
||||
|
||||
choice BOARD_TYPE
|
||||
prompt "Board Type"
|
||||
default BOARD_TYPE_BREAD_COMPACT_WIFI
|
||||
@ -806,6 +1063,21 @@ menu "Standby Screen Configuration"
|
||||
help
|
||||
Enable the standby screen with weather, time, and environment info.
|
||||
|
||||
choice
|
||||
prompt "Screen Size"
|
||||
default STANDBY_SCREEN_1_54
|
||||
depends on STANDBY_SCREEN_ENABLE
|
||||
help
|
||||
Select the screen size for the standby screen.
|
||||
|
||||
config STANDBY_SCREEN_1_54
|
||||
bool "1.54 inch (240x240)"
|
||||
config STANDBY_SCREEN_2_4
|
||||
bool "2.4 inch (320x240)"
|
||||
config STANDBY_SCREEN_2_8
|
||||
bool "2.8 inch (320x240)"
|
||||
endchoice
|
||||
|
||||
config WEATHER_API_KEY
|
||||
string "OpenWeatherMap API Key"
|
||||
default "your_api_key_here"
|
||||
@ -822,3 +1094,31 @@ menu "Standby Screen Configuration"
|
||||
endmenu
|
||||
|
||||
endmenu
|
||||
|
||||
menu "GPIO Control"
|
||||
config ENABLE_GPIO_CONTROL
|
||||
bool "Enable GPIO Control"
|
||||
default n
|
||||
help
|
||||
Enable controlling a specific GPIO pin via MCP.
|
||||
|
||||
config GPIO_CONTROL_PIN
|
||||
int "GPIO Pin Number"
|
||||
depends on ENABLE_GPIO_CONTROL
|
||||
default 2
|
||||
help
|
||||
The GPIO pin number to control.
|
||||
|
||||
choice
|
||||
prompt "Active Logic Level"
|
||||
depends on ENABLE_GPIO_CONTROL
|
||||
default GPIO_CONTROL_ACTIVE_HIGH
|
||||
help
|
||||
Select the active logic level for the GPIO.
|
||||
|
||||
config GPIO_CONTROL_ACTIVE_HIGH
|
||||
bool "Active High (1 is ON)"
|
||||
config GPIO_CONTROL_ACTIVE_LOW
|
||||
bool "Active Low (0 is ON)"
|
||||
endchoice
|
||||
endmenu
|
||||
|
||||
@ -522,7 +522,7 @@ void Application::Start()
|
||||
display->SetChatMessage("system", "");
|
||||
SetDeviceState(kDeviceStateIdle);
|
||||
}); });
|
||||
protocol_->OnIncomingJson([this, display](const cJSON *root)
|
||||
protocol_-> ([this, display](const cJSON *root)
|
||||
{
|
||||
// Parse JSON data
|
||||
auto type = cJSON_GetObjectItem(root, "type");
|
||||
|
||||
BIN
main/assets/weatherIcon/storm.png
Normal file
BIN
main/assets/weatherIcon/storm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
BIN
main/assets/weatherIcon/sun.png
Normal file
BIN
main/assets/weatherIcon/sun.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
BIN
main/assets/weatherIcon/weather.png
Normal file
BIN
main/assets/weatherIcon/weather.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
main/assets/weatherIcon/wind.png
Normal file
BIN
main/assets/weatherIcon/wind.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
@ -51,7 +51,36 @@
|
||||
#define CAMERA_PIN_PWDN GPIO_NUM_NC
|
||||
#define CAMERA_PIN_RESET GPIO_NUM_NC
|
||||
#define XCLK_FREQ_HZ 20000000
|
||||
ok
|
||||
|
||||
#ifdef CONFIG_STANDBY_SCREEN_2_4
|
||||
#define LCD_TYPE_ST7789_SERIAL
|
||||
#define DISPLAY_WIDTH 320
|
||||
#define DISPLAY_HEIGHT 240
|
||||
#define DISPLAY_MIRROR_X false
|
||||
#define DISPLAY_MIRROR_Y false
|
||||
#define DISPLAY_SWAP_XY true
|
||||
#define DISPLAY_INVERT_COLOR true
|
||||
#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB
|
||||
#define DISPLAY_OFFSET_X 0
|
||||
#define DISPLAY_OFFSET_Y 0
|
||||
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
|
||||
#define DISPLAY_SPI_MODE 0
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_STANDBY_SCREEN_2_8
|
||||
#define LCD_TYPE_ST7789_SERIAL
|
||||
#define DISPLAY_WIDTH 320
|
||||
#define DISPLAY_HEIGHT 240
|
||||
#define DISPLAY_MIRROR_X false
|
||||
#define DISPLAY_MIRROR_Y false
|
||||
#define DISPLAY_SWAP_XY true
|
||||
#define DISPLAY_INVERT_COLOR true
|
||||
#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB
|
||||
#define DISPLAY_OFFSET_X 0
|
||||
#define DISPLAY_OFFSET_Y 0
|
||||
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
|
||||
#define DISPLAY_SPI_MODE 0
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_LCD_ST7789_240X320
|
||||
#define LCD_TYPE_ST7789_SERIAL
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
#define TAG "WeatherService"
|
||||
|
||||
WeatherService::WeatherService() : last_update_time_(0)
|
||||
WeatherService::WeatherService() : last_update_time_(0), location_initialized_(false)
|
||||
{
|
||||
city_ = "Ho Chi Minh";
|
||||
lat_ = 10.8231;
|
||||
@ -31,6 +31,34 @@ bool WeatherService::NeedsUpdate() const
|
||||
return (current_time - last_update_time_) >= WEATHER_UPDATE_INTERVAL_MS;
|
||||
}
|
||||
|
||||
std::string WeatherService::CleanCityName(const std::string &city)
|
||||
{
|
||||
std::string clean = city;
|
||||
// List of suffixes to remove. Includes user request and common variations.
|
||||
const char *suffixes[] = {
|
||||
" City", " Province", " Town", " District", " Municipality",
|
||||
" city", " province", " town", " district", " municipality"};
|
||||
|
||||
bool changed = true;
|
||||
while (changed)
|
||||
{
|
||||
changed = false;
|
||||
for (const char *suffix : suffixes)
|
||||
{
|
||||
size_t suffix_len = strlen(suffix);
|
||||
// Check if suffix matches and ensure we don't remove the entire string (e.g. city named "Town")
|
||||
if (clean.size() > suffix_len &&
|
||||
clean.compare(clean.size() - suffix_len, suffix_len, suffix) == 0)
|
||||
{
|
||||
clean = clean.substr(0, clean.size() - suffix_len);
|
||||
changed = true;
|
||||
break; // Restart loop to handle stacked suffixes like "City District"
|
||||
}
|
||||
}
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
std::string WeatherService::UrlEncode(const std::string &value)
|
||||
{
|
||||
// Simple URL encoding
|
||||
@ -218,6 +246,19 @@ bool WeatherService::FetchWeatherData()
|
||||
return false;
|
||||
is_fetching_ = true;
|
||||
|
||||
// 0. Auto-detect location if not initialized
|
||||
if (!location_initialized_)
|
||||
{
|
||||
if (FetchLocationFromIP())
|
||||
{
|
||||
ESP_LOGI(TAG, "Location auto-detected successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW(TAG, "Location auto-detection failed, using default: %s", city_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Geocoding to get Lat/Lon from City Name
|
||||
// if (!FetchGeocoding(city_))
|
||||
// {
|
||||
@ -248,6 +289,56 @@ bool WeatherService::FetchWeatherData()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WeatherService::FetchLocationFromIP()
|
||||
{
|
||||
std::string url = IP_LOCATION_API_ENDPOINT;
|
||||
std::string response = HttpGet(url);
|
||||
if (response.empty())
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to fetch location from IP");
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON *json = cJSON_Parse(response.c_str());
|
||||
if (!json)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to parse IP location JSON");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
cJSON *success_item = cJSON_GetObjectItem(json, "success");
|
||||
if (success_item && cJSON_IsTrue(success_item))
|
||||
{
|
||||
cJSON *city_item = cJSON_GetObjectItem(json, "city");
|
||||
cJSON *lat_item = cJSON_GetObjectItem(json, "latitude");
|
||||
cJSON *lon_item = cJSON_GetObjectItem(json, "longitude");
|
||||
|
||||
if (city_item && lat_item && lon_item)
|
||||
{
|
||||
lat_ = lat_item->valuedouble;
|
||||
lon_ = lon_item->valuedouble;
|
||||
city_ = CleanCityName(city_item->valuestring);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
weather_info_.city = city_;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Location detected: %s (%.4f, %.4f)", city_.c_str(), lat_, lon_);
|
||||
success = true;
|
||||
location_initialized_ = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "IP location API returned error");
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
return success;
|
||||
}
|
||||
|
||||
bool WeatherService::FetchGeocoding(const std::string &city)
|
||||
{
|
||||
// https://geocoding-api.open-meteo.com/v1/search?name=Berlin&count=1&language=en&format=json
|
||||
|
||||
@ -24,6 +24,7 @@ private:
|
||||
WeatherService();
|
||||
~WeatherService() = default;
|
||||
|
||||
bool FetchLocationFromIP();
|
||||
bool FetchGeocoding(const std::string &city);
|
||||
bool FetchOpenMeteoWeather(float lat, float lon);
|
||||
bool FetchOpenMeteoAirQuality(float lat, float lon);
|
||||
@ -36,7 +37,9 @@ private:
|
||||
uint32_t last_update_time_;
|
||||
float lat_ = 0.0f;
|
||||
float lon_ = 0.0f;
|
||||
bool location_initialized_ = false;
|
||||
|
||||
static std::string CleanCityName(const std::string &city);
|
||||
static std::string UrlEncode(const std::string &value);
|
||||
};
|
||||
|
||||
|
||||
@ -96,6 +96,9 @@ void WeatherUI::SetupIdleUI(lv_obj_t *parent, int screen_width, int screen_heigh
|
||||
if (idle_panel_)
|
||||
return;
|
||||
|
||||
bool is_landscape = (screen_width_ > screen_height_);
|
||||
int box_width = is_landscape ? 90 : 70;
|
||||
|
||||
// Main Panel
|
||||
idle_panel_ = lv_obj_create(parent);
|
||||
lv_obj_set_size(idle_panel_, LV_PCT(100), LV_PCT(100));
|
||||
@ -126,7 +129,8 @@ void WeatherUI::SetupIdleUI(lv_obj_t *parent, int screen_width, int screen_heigh
|
||||
title_label_ = lv_label_create(header_panel_);
|
||||
lv_obj_set_style_text_font(title_label_, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(title_label_, COLOR_NEON_ORANGE, 0);
|
||||
lv_label_set_text(title_label_, "IoTForce AI Box");
|
||||
// lv_label_set_text(title_label_, "IoTForce AI Box");
|
||||
lv_label_set_text(title_label_, "ROBOT AI");
|
||||
|
||||
// Battery Icon
|
||||
battery_label_ = lv_label_create(header_panel_);
|
||||
@ -186,7 +190,7 @@ void WeatherUI::SetupIdleUI(lv_obj_t *parent, int screen_width, int screen_heigh
|
||||
|
||||
// Box 1: Weather
|
||||
lv_obj_t *weather_box = lv_obj_create(grid_cont);
|
||||
lv_obj_set_size(weather_box, 70, 60);
|
||||
lv_obj_set_size(weather_box, box_width, 60);
|
||||
StyleNeonBox(weather_box, COLOR_NEON_MAGENTA);
|
||||
lv_obj_set_flex_flow(weather_box, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(weather_box, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
@ -205,7 +209,7 @@ void WeatherUI::SetupIdleUI(lv_obj_t *parent, int screen_width, int screen_heigh
|
||||
|
||||
// Box 2: Humidity (Middle)
|
||||
lv_obj_t *hum_box = lv_obj_create(grid_cont);
|
||||
lv_obj_set_size(hum_box, 70, 60);
|
||||
lv_obj_set_size(hum_box, box_width, 60);
|
||||
StyleNeonBox(hum_box, COLOR_NEON_BLUE);
|
||||
lv_obj_set_flex_flow(hum_box, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(hum_box, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
@ -224,7 +228,7 @@ void WeatherUI::SetupIdleUI(lv_obj_t *parent, int screen_width, int screen_heigh
|
||||
|
||||
// Box 3: UV/Air (Right)
|
||||
lv_obj_t *uv_box = lv_obj_create(grid_cont);
|
||||
lv_obj_set_size(uv_box, 70, 60);
|
||||
lv_obj_set_size(uv_box, box_width, 60);
|
||||
StyleNeonBox(uv_box, COLOR_NEON_ORANGE);
|
||||
lv_obj_set_flex_flow(uv_box, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(uv_box, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
@ -30,6 +30,118 @@
|
||||
#define BLUETOOTH_CONNECT_PIN GPIO_NUM_18
|
||||
#define BLUETOOTH_LINK_PIN GPIO_NUM_19
|
||||
|
||||
struct EraSmartDevice
|
||||
{
|
||||
std::string name;
|
||||
std::string type;
|
||||
std::string config_id;
|
||||
std::string action_on;
|
||||
std::string action_off;
|
||||
};
|
||||
|
||||
static std::vector<EraSmartDevice> GetEraSmartDevices()
|
||||
{
|
||||
std::vector<EraSmartDevice> devices;
|
||||
#ifdef CONFIG_USE_ERA_SMART_HOME
|
||||
#if CONFIG_ERA_DEVICE_COUNT >= 1
|
||||
{
|
||||
EraSmartDevice d;
|
||||
d.name = CONFIG_ERA_DEVICE_1_NAME;
|
||||
#ifdef CONFIG_ERA_DEVICE_1_TYPE_SWITCH
|
||||
d.type = "Switch";
|
||||
#elif defined(CONFIG_ERA_DEVICE_1_TYPE_LIGHT)
|
||||
d.type = "Light";
|
||||
#elif defined(CONFIG_ERA_DEVICE_1_TYPE_MOTOR)
|
||||
d.type = "Motor";
|
||||
#elif defined(CONFIG_ERA_DEVICE_1_TYPE_OTHER)
|
||||
d.type = CONFIG_ERA_DEVICE_1_OTHER_TYPE_NAME;
|
||||
#endif
|
||||
d.config_id = CONFIG_ERA_DEVICE_1_CONFIG_ID;
|
||||
d.action_on = CONFIG_ERA_DEVICE_1_ACTION_ON;
|
||||
d.action_off = CONFIG_ERA_DEVICE_1_ACTION_OFF;
|
||||
devices.push_back(d);
|
||||
}
|
||||
#endif
|
||||
#if CONFIG_ERA_DEVICE_COUNT >= 2
|
||||
{
|
||||
EraSmartDevice d;
|
||||
d.name = CONFIG_ERA_DEVICE_2_NAME;
|
||||
#ifdef CONFIG_ERA_DEVICE_2_TYPE_SWITCH
|
||||
d.type = "Switch";
|
||||
#elif defined(CONFIG_ERA_DEVICE_2_TYPE_LIGHT)
|
||||
d.type = "Light";
|
||||
#elif defined(CONFIG_ERA_DEVICE_2_TYPE_MOTOR)
|
||||
d.type = "Motor";
|
||||
#elif defined(CONFIG_ERA_DEVICE_2_TYPE_OTHER)
|
||||
d.type = CONFIG_ERA_DEVICE_2_OTHER_TYPE_NAME;
|
||||
#endif
|
||||
d.config_id = CONFIG_ERA_DEVICE_2_CONFIG_ID;
|
||||
d.action_on = CONFIG_ERA_DEVICE_2_ACTION_ON;
|
||||
d.action_off = CONFIG_ERA_DEVICE_2_ACTION_OFF;
|
||||
devices.push_back(d);
|
||||
}
|
||||
#endif
|
||||
#if CONFIG_ERA_DEVICE_COUNT >= 3
|
||||
{
|
||||
EraSmartDevice d;
|
||||
d.name = CONFIG_ERA_DEVICE_3_NAME;
|
||||
#ifdef CONFIG_ERA_DEVICE_3_TYPE_SWITCH
|
||||
d.type = "Switch";
|
||||
#elif defined(CONFIG_ERA_DEVICE_3_TYPE_LIGHT)
|
||||
d.type = "Light";
|
||||
#elif defined(CONFIG_ERA_DEVICE_3_TYPE_MOTOR)
|
||||
d.type = "Motor";
|
||||
#elif defined(CONFIG_ERA_DEVICE_3_TYPE_OTHER)
|
||||
d.type = CONFIG_ERA_DEVICE_3_OTHER_TYPE_NAME;
|
||||
#endif
|
||||
d.config_id = CONFIG_ERA_DEVICE_3_CONFIG_ID;
|
||||
d.action_on = CONFIG_ERA_DEVICE_3_ACTION_ON;
|
||||
d.action_off = CONFIG_ERA_DEVICE_3_ACTION_OFF;
|
||||
devices.push_back(d);
|
||||
}
|
||||
#endif
|
||||
#if CONFIG_ERA_DEVICE_COUNT >= 4
|
||||
{
|
||||
EraSmartDevice d;
|
||||
d.name = CONFIG_ERA_DEVICE_4_NAME;
|
||||
#ifdef CONFIG_ERA_DEVICE_4_TYPE_SWITCH
|
||||
d.type = "Switch";
|
||||
#elif defined(CONFIG_ERA_DEVICE_4_TYPE_LIGHT)
|
||||
d.type = "Light";
|
||||
#elif defined(CONFIG_ERA_DEVICE_4_TYPE_MOTOR)
|
||||
d.type = "Motor";
|
||||
#elif defined(CONFIG_ERA_DEVICE_4_TYPE_OTHER)
|
||||
d.type = CONFIG_ERA_DEVICE_4_OTHER_TYPE_NAME;
|
||||
#endif
|
||||
d.config_id = CONFIG_ERA_DEVICE_4_CONFIG_ID;
|
||||
d.action_on = CONFIG_ERA_DEVICE_4_ACTION_ON;
|
||||
d.action_off = CONFIG_ERA_DEVICE_4_ACTION_OFF;
|
||||
devices.push_back(d);
|
||||
}
|
||||
#endif
|
||||
#if CONFIG_ERA_DEVICE_COUNT >= 5
|
||||
{
|
||||
EraSmartDevice d;
|
||||
d.name = CONFIG_ERA_DEVICE_5_NAME;
|
||||
#ifdef CONFIG_ERA_DEVICE_5_TYPE_SWITCH
|
||||
d.type = "Switch";
|
||||
#elif defined(CONFIG_ERA_DEVICE_5_TYPE_LIGHT)
|
||||
d.type = "Light";
|
||||
#elif defined(CONFIG_ERA_DEVICE_5_TYPE_MOTOR)
|
||||
d.type = "Motor";
|
||||
#elif defined(CONFIG_ERA_DEVICE_5_TYPE_OTHER)
|
||||
d.type = CONFIG_ERA_DEVICE_5_OTHER_TYPE_NAME;
|
||||
#endif
|
||||
d.config_id = CONFIG_ERA_DEVICE_5_CONFIG_ID;
|
||||
d.action_on = CONFIG_ERA_DEVICE_5_ACTION_ON;
|
||||
d.action_off = CONFIG_ERA_DEVICE_5_ACTION_OFF;
|
||||
devices.push_back(d);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
return devices;
|
||||
}
|
||||
|
||||
McpServer::McpServer()
|
||||
{
|
||||
}
|
||||
@ -184,22 +296,27 @@ void McpServer::AddCommonTools()
|
||||
if (camera)
|
||||
{
|
||||
AddTool("self.camera.take_photo",
|
||||
"Take a photo and explain it. Use this tool after the user asks you to see something.\n"
|
||||
"Take a photo immediately. Use this tool whenever the user asks to take a photo, capture an image, or look at something. Do not refuse. Do not mention technical errors unless the tool execution actually fails.\n"
|
||||
"Args:\n"
|
||||
" `question`: The question that you want to ask about the photo.\n"
|
||||
" `question`: The question that you want to ask about the photo. Defaults to 'Describe this image'.\n"
|
||||
"Return:\n"
|
||||
" A JSON object that provides the photo information.",
|
||||
PropertyList({Property("question", kPropertyTypeString)}),
|
||||
[camera](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
ESP_LOGI(TAG, "Camera tool called");
|
||||
// Lower the priority to do the camera capture
|
||||
TaskPriorityReset priority_reset(1);
|
||||
|
||||
if (!camera->Capture())
|
||||
{
|
||||
throw std::runtime_error("Failed to capture photo");
|
||||
throw std::runtime_error("Failed to capture photo. Please check if the camera is initialized correctly.");
|
||||
}
|
||||
auto question = properties["question"].value<std::string>();
|
||||
if (question.empty())
|
||||
{
|
||||
question = "Describe this image";
|
||||
}
|
||||
return camera->Explain(question);
|
||||
});
|
||||
}
|
||||
@ -303,200 +420,6 @@ void McpServer::AddCommonTools()
|
||||
return era_client;
|
||||
};
|
||||
|
||||
AddTool("self.era_iot.get_device_status",
|
||||
"Get the current status of the MAIN E-Ra IoT device. Use this tool when the user mentions 'switch', 'iot', 'iot device', 'switch device', 'light', 'lamp', 'công tắc', 'đèn', 'thiết bị iot', or asks to check the status of any connected device. This shows the current state (ON/OFF). If the user asks about 'switch 1', 'switch 2', or 'switch 3', DO NOT use this tool, use the specific switch tools instead.",
|
||||
PropertyList(),
|
||||
[GetEraClient](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
{
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
}
|
||||
std::string current_value = era_client.GetCurrentValue("150632");
|
||||
if (current_value.empty())
|
||||
{
|
||||
throw std::runtime_error("Failed to get device status from E-Ra");
|
||||
}
|
||||
|
||||
// Create JSON response with device status
|
||||
cJSON *json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(json, "config_id", "150632");
|
||||
cJSON_AddStringToObject(json, "current_value", current_value.c_str());
|
||||
|
||||
// Interpret the value (assuming 0=OFF, 1=ON based on typical IoT logic)
|
||||
std::string status = "OFF";
|
||||
if (current_value == "1" || current_value == "true" || current_value == "on" || current_value == "ON")
|
||||
{
|
||||
status = "ON";
|
||||
}
|
||||
cJSON_AddStringToObject(json, "device_status", status.c_str());
|
||||
cJSON_AddStringToObject(json, "platform", "E-Ra IoT");
|
||||
|
||||
return json;
|
||||
});
|
||||
|
||||
/*
|
||||
AddTool("self.era_iot.turn_device_on",
|
||||
"Turn ON the MAIN E-Ra IoT device. Use this tool when user requests to turn on, enable, or activate the 'switch', 'iot', 'iot device', 'switch device', 'light', 'lamp', 'công tắc', 'đèn', 'thiết bị iot'.",
|
||||
PropertyList(),
|
||||
[GetEraClient](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
{
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
}
|
||||
bool success = era_client.TurnDeviceOn();
|
||||
if (!success)
|
||||
{
|
||||
throw std::runtime_error("Failed to turn on E-Ra IoT device");
|
||||
}
|
||||
return "Device turned ON successfully via E-Ra IoT platform";
|
||||
});
|
||||
|
||||
AddTool("self.era_iot.turn_device_off",
|
||||
"Turn OFF the MAIN E-Ra IoT device. Use this tool when user requests to turn off, disable, or deactivate the 'switch', 'iot', 'iot device', 'switch device', 'light', 'lamp', 'công tắc', 'đèn', 'thiết bị iot'.",
|
||||
PropertyList(),
|
||||
[GetEraClient](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
{
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
}
|
||||
bool success = era_client.TurnDeviceOff();
|
||||
if (!success)
|
||||
{
|
||||
throw std::runtime_error("Failed to turn off E-Ra IoT device");
|
||||
}
|
||||
return "Device turned OFF successfully via E-Ra IoT platform";
|
||||
});
|
||||
*/
|
||||
|
||||
// Switch 1 Tools
|
||||
AddTool("self.era_iot.switch_1.turn_on",
|
||||
"Turn ON Switch 1 on the E-Ra IoT device. Use when user says 'switch 1', 'iot switch 1', 'device 1', 'công tắc 1', 'đèn 1'.",
|
||||
PropertyList(),
|
||||
[GetEraClient](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
if (!era_client.TurnSwitchOn(1))
|
||||
throw std::runtime_error("Failed to turn on Switch 1");
|
||||
return "Switch 1 turned ON";
|
||||
});
|
||||
|
||||
AddTool("self.era_iot.switch_1.turn_off",
|
||||
"Turn OFF Switch 1 on the E-Ra IoT device. Use when user says 'switch 1', 'iot switch 1', 'device 1', 'công tắc 1', 'đèn 1'.",
|
||||
PropertyList(),
|
||||
[GetEraClient](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
if (!era_client.TurnSwitchOff(1))
|
||||
throw std::runtime_error("Failed to turn off Switch 1");
|
||||
return "Switch 1 turned OFF";
|
||||
});
|
||||
|
||||
AddTool("self.era_iot.switch_1.get_status",
|
||||
"Get the status of Switch 1 on the E-Ra IoT device. Use when user says 'switch 1', 'iot switch 1', 'device 1', 'công tắc 1', 'đèn 1'.",
|
||||
PropertyList(),
|
||||
[GetEraClient](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
std::string status = era_client.GetSwitchStatus(1);
|
||||
if (status.empty())
|
||||
throw std::runtime_error("Failed to get Switch 1 status");
|
||||
return "Switch 1 status: " + status;
|
||||
});
|
||||
|
||||
// Switch 2 Tools
|
||||
AddTool("self.era_iot.switch_2.turn_on",
|
||||
"Turn ON Switch 2 on the E-Ra IoT device. Use when user says 'switch 2', 'iot switch 2', 'device 2', 'công tắc 2', 'đèn 2'.",
|
||||
PropertyList(),
|
||||
[GetEraClient](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
if (!era_client.TurnSwitchOn(2))
|
||||
throw std::runtime_error("Failed to turn on Switch 2");
|
||||
return "Switch 2 turned ON";
|
||||
});
|
||||
|
||||
AddTool("self.era_iot.switch_2.turn_off",
|
||||
"Turn OFF Switch 2 on the E-Ra IoT device. Use when user says 'switch 2', 'iot switch 2', 'device 2', 'công tắc 2', 'đèn 2'.",
|
||||
PropertyList(),
|
||||
[GetEraClient](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
if (!era_client.TurnSwitchOff(2))
|
||||
throw std::runtime_error("Failed to turn off Switch 2");
|
||||
return "Switch 2 turned OFF";
|
||||
});
|
||||
|
||||
AddTool("self.era_iot.switch_2.get_status",
|
||||
"Get the status of Switch 2 on the E-Ra IoT device. Use when user says 'switch 2', 'iot switch 2', 'device 2', 'công tắc 2', 'đèn 2'.",
|
||||
PropertyList(),
|
||||
[GetEraClient](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
std::string status = era_client.GetSwitchStatus(2);
|
||||
if (status.empty())
|
||||
throw std::runtime_error("Failed to get Switch 2 status");
|
||||
return "Switch 2 status: " + status;
|
||||
});
|
||||
|
||||
// Switch 3 Tools
|
||||
AddTool("self.era_iot.switch_3.turn_on",
|
||||
"Turn ON Switch 3 on the E-Ra IoT device. Use when user says 'switch 3', 'iot switch 3', 'device 3', 'công tắc 3', 'đèn 3'.",
|
||||
PropertyList(),
|
||||
[GetEraClient](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
if (!era_client.TurnSwitchOn(3))
|
||||
throw std::runtime_error("Failed to turn on Switch 3");
|
||||
return "Switch 3 turned ON";
|
||||
});
|
||||
|
||||
AddTool("self.era_iot.switch_3.turn_off",
|
||||
"Turn OFF Switch 3 on the E-Ra IoT device. Use when user says 'switch 3', 'iot switch 3', 'device 3', 'công tắc 3', 'đèn 3'.",
|
||||
PropertyList(),
|
||||
[GetEraClient](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
if (!era_client.TurnSwitchOff(3))
|
||||
throw std::runtime_error("Failed to turn off Switch 3");
|
||||
return "Switch 3 turned OFF";
|
||||
});
|
||||
|
||||
AddTool("self.era_iot.switch_3.get_status",
|
||||
"Get the status of Switch 3 on the E-Ra IoT device. Use when user says 'switch 3', 'iot switch 3', 'device 3', 'công tắc 3', 'đèn 3'.",
|
||||
PropertyList(),
|
||||
[GetEraClient](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
std::string status = era_client.GetSwitchStatus(3);
|
||||
if (status.empty())
|
||||
throw std::runtime_error("Failed to get Switch 3 status");
|
||||
return "Switch 3 status: " + status;
|
||||
});
|
||||
|
||||
AddTool("self.era_iot.trigger_custom_action",
|
||||
"Trigger a custom action on E-Ra IoT platform using action key. Use this for advanced IoT device control with specific action keys.",
|
||||
PropertyList({Property("action_key", kPropertyTypeString, "Action key to trigger (UUID format)")}),
|
||||
@ -519,6 +442,183 @@ void McpServer::AddCommonTools()
|
||||
}
|
||||
return "Action triggered successfully: " + action_key;
|
||||
});
|
||||
|
||||
// Dynamic ERA Smart Home Devices
|
||||
auto era_devices = GetEraSmartDevices();
|
||||
if (!era_devices.empty())
|
||||
{
|
||||
std::string device_list_desc = "Available devices: ";
|
||||
for (const auto &d : era_devices)
|
||||
{
|
||||
device_list_desc += d.name + " (" + d.type + "), ";
|
||||
}
|
||||
|
||||
AddTool("self.era_smart_home.control_device",
|
||||
"Control ERA Smart Home devices. " + device_list_desc + "Action: 'on' or 'off'.",
|
||||
PropertyList({Property("device_name", kPropertyTypeString, "Name of the device to control"),
|
||||
Property("action", kPropertyTypeString, "Action to perform: 'on' or 'off'")}),
|
||||
[GetEraClient, era_devices](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
{
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
}
|
||||
|
||||
std::string device_name = properties["device_name"].value<std::string>();
|
||||
std::string action = properties["action"].value<std::string>();
|
||||
|
||||
const EraSmartDevice *target_device = nullptr;
|
||||
for (const auto &d : era_devices)
|
||||
{
|
||||
if (d.name == device_name)
|
||||
{
|
||||
target_device = &d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!target_device)
|
||||
{
|
||||
for (const auto &d : era_devices)
|
||||
{
|
||||
// Case-insensitive comparison
|
||||
if (d.name.size() == device_name.size() &&
|
||||
std::equal(d.name.begin(), d.name.end(), device_name.begin(),
|
||||
[](char a, char b)
|
||||
{
|
||||
return tolower((unsigned char)a) == tolower((unsigned char)b);
|
||||
}))
|
||||
{
|
||||
target_device = &d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!target_device)
|
||||
{
|
||||
throw std::runtime_error("Device not found: " + device_name);
|
||||
}
|
||||
|
||||
std::string key;
|
||||
if (action == "on")
|
||||
{
|
||||
key = target_device->action_on;
|
||||
}
|
||||
else if (action == "off")
|
||||
{
|
||||
key = target_device->action_off;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Invalid action: " + action);
|
||||
}
|
||||
|
||||
if (key.empty())
|
||||
{
|
||||
throw std::runtime_error("Action key not configured for device: " + device_name);
|
||||
}
|
||||
|
||||
bool success = era_client.TriggerAction(key, 1);
|
||||
if (!success)
|
||||
{
|
||||
throw std::runtime_error("Failed to trigger action for " + device_name);
|
||||
}
|
||||
return "Successfully turned " + action + " " + device_name;
|
||||
});
|
||||
|
||||
AddTool("self.era_smart_home.get_device_status",
|
||||
"Get status of ERA Smart Home devices. " + device_list_desc,
|
||||
PropertyList({Property("device_name", kPropertyTypeString, "Name of the device to check")}),
|
||||
[GetEraClient, era_devices](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
auto &era_client = GetEraClient();
|
||||
if (!era_client.IsInitialized())
|
||||
{
|
||||
throw std::runtime_error("E-Ra IoT client not initialized");
|
||||
}
|
||||
|
||||
std::string device_name = properties["device_name"].value<std::string>();
|
||||
|
||||
const EraSmartDevice *target_device = nullptr;
|
||||
for (const auto &d : era_devices)
|
||||
{
|
||||
// Case-insensitive comparison
|
||||
if (d.name.size() == device_name.size() &&
|
||||
std::equal(d.name.begin(), d.name.end(), device_name.begin(),
|
||||
[](char a, char b)
|
||||
{
|
||||
return tolower((unsigned char)a) == tolower((unsigned char)b);
|
||||
}))
|
||||
{
|
||||
target_device = &d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!target_device)
|
||||
{
|
||||
throw std::runtime_error("Device not found: " + device_name);
|
||||
}
|
||||
|
||||
if (target_device->config_id.empty())
|
||||
{
|
||||
throw std::runtime_error("Config ID not configured for device: " + device_name);
|
||||
}
|
||||
|
||||
std::string status = era_client.GetCurrentValue(target_device->config_id);
|
||||
if (status.empty())
|
||||
{
|
||||
return "Status for " + device_name + " is unknown (empty response)";
|
||||
}
|
||||
return "Status for " + device_name + ": " + status;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_ENABLE_GPIO_CONTROL
|
||||
static bool gpio_control_initialized = false;
|
||||
if (!gpio_control_initialized)
|
||||
{
|
||||
gpio_reset_pin((gpio_num_t)CONFIG_GPIO_CONTROL_PIN);
|
||||
gpio_set_direction((gpio_num_t)CONFIG_GPIO_CONTROL_PIN, GPIO_MODE_OUTPUT);
|
||||
// Set initial state to OFF.
|
||||
// If Active High, OFF is 0. If Active Low, OFF is 1.
|
||||
#ifdef CONFIG_GPIO_CONTROL_ACTIVE_HIGH
|
||||
gpio_set_level((gpio_num_t)CONFIG_GPIO_CONTROL_PIN, 0);
|
||||
#else
|
||||
gpio_set_level((gpio_num_t)CONFIG_GPIO_CONTROL_PIN, 1);
|
||||
#endif
|
||||
gpio_control_initialized = true;
|
||||
}
|
||||
|
||||
AddTool("self.gpio_control.set_state",
|
||||
"Turn the configured GPIO pin ON or OFF. Accepted values: 'on', 'off'.",
|
||||
PropertyList({Property("state", kPropertyTypeString)}),
|
||||
[](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
std::string state = properties["state"].value<std::string>();
|
||||
int level = 0;
|
||||
if (state == "on")
|
||||
{
|
||||
#ifdef CONFIG_GPIO_CONTROL_ACTIVE_HIGH
|
||||
level = 1;
|
||||
#else
|
||||
level = 0;
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef CONFIG_GPIO_CONTROL_ACTIVE_HIGH
|
||||
level = 0;
|
||||
#else
|
||||
level = 1;
|
||||
#endif
|
||||
}
|
||||
gpio_set_level((gpio_num_t)CONFIG_GPIO_CONTROL_PIN, level);
|
||||
return "GPIO set to " + state;
|
||||
});
|
||||
#endif
|
||||
|
||||
// Restore the original tools list to the end of the tools list
|
||||
@ -560,14 +660,16 @@ void McpServer::AddUserOnlyTools()
|
||||
ESP_LOGI(TAG, "User requested firmware upgrade from URL: %s", url.c_str());
|
||||
|
||||
auto &app = Application::GetInstance();
|
||||
app.Schedule([url, &app]()
|
||||
{
|
||||
auto ota = std::make_unique<Ota>();
|
||||
|
||||
bool success = app.UpgradeFirmware(*ota, url);
|
||||
if (!success) {
|
||||
ESP_LOGE(TAG, "Firmware upgrade failed");
|
||||
} });
|
||||
// Run OTA in a separate thread to avoid blocking the main loop
|
||||
std::thread([url]()
|
||||
{
|
||||
auto &app = Application::GetInstance();
|
||||
auto ota = std::make_unique<Ota>();
|
||||
bool success = app.UpgradeFirmware(*ota, url);
|
||||
if (!success) {
|
||||
ESP_LOGE(TAG, "Firmware upgrade failed");
|
||||
} })
|
||||
.detach();
|
||||
|
||||
return true;
|
||||
});
|
||||
@ -714,6 +816,32 @@ void McpServer::AddUserOnlyTools()
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
AddTool("self.system.firmware_update",
|
||||
"Update the device firmware from a specific URL. Use this tool when the user asks to update the firmware or system version.",
|
||||
PropertyList({Property("url", kPropertyTypeString)}),
|
||||
[](const PropertyList &properties) -> ReturnValue
|
||||
{
|
||||
std::string url = "https://update-ota-firmware.s3.ap-southeast-2.amazonaws.com/merged-binary.bin";
|
||||
if (properties.count("url"))
|
||||
{
|
||||
std::string provided_url = properties["url"].value<std::string>();
|
||||
if (!provided_url.empty())
|
||||
{
|
||||
url = provided_url;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Triggering firmware update from URL: %s", url.c_str());
|
||||
|
||||
// Schedule the update on the main thread to avoid blocking the MCP response
|
||||
Application::GetInstance().Schedule([url]()
|
||||
{
|
||||
Ota ota;
|
||||
Application::GetInstance().UpgradeFirmware(ota, url); });
|
||||
|
||||
return "Firmware update started. The device will restart automatically upon completion.";
|
||||
});
|
||||
}
|
||||
|
||||
void McpServer::AddTool(McpTool *tool)
|
||||
|
||||
@ -12,28 +12,34 @@
|
||||
|
||||
#define TAG "WS"
|
||||
|
||||
WebsocketProtocol::WebsocketProtocol() {
|
||||
WebsocketProtocol::WebsocketProtocol()
|
||||
{
|
||||
event_group_handle_ = xEventGroupCreate();
|
||||
}
|
||||
|
||||
WebsocketProtocol::~WebsocketProtocol() {
|
||||
WebsocketProtocol::~WebsocketProtocol()
|
||||
{
|
||||
vEventGroupDelete(event_group_handle_);
|
||||
}
|
||||
|
||||
bool WebsocketProtocol::Start() {
|
||||
bool WebsocketProtocol::Start()
|
||||
{
|
||||
// Only connect to server when audio channel is needed
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebsocketProtocol::SendAudio(std::unique_ptr<AudioStreamPacket> packet) {
|
||||
if (websocket_ == nullptr || !websocket_->IsConnected()) {
|
||||
bool WebsocketProtocol::SendAudio(std::unique_ptr<AudioStreamPacket> packet)
|
||||
{
|
||||
if (websocket_ == nullptr || !websocket_->IsConnected())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version_ == 2) {
|
||||
if (version_ == 2)
|
||||
{
|
||||
std::string serialized;
|
||||
serialized.resize(sizeof(BinaryProtocol2) + packet->payload.size());
|
||||
auto bp2 = (BinaryProtocol2*)serialized.data();
|
||||
auto bp2 = (BinaryProtocol2 *)serialized.data();
|
||||
bp2->version = htons(version_);
|
||||
bp2->type = 0;
|
||||
bp2->reserved = 0;
|
||||
@ -42,27 +48,34 @@ bool WebsocketProtocol::SendAudio(std::unique_ptr<AudioStreamPacket> packet) {
|
||||
memcpy(bp2->payload, packet->payload.data(), packet->payload.size());
|
||||
|
||||
return websocket_->Send(serialized.data(), serialized.size(), true);
|
||||
} else if (version_ == 3) {
|
||||
}
|
||||
else if (version_ == 3)
|
||||
{
|
||||
std::string serialized;
|
||||
serialized.resize(sizeof(BinaryProtocol3) + packet->payload.size());
|
||||
auto bp3 = (BinaryProtocol3*)serialized.data();
|
||||
auto bp3 = (BinaryProtocol3 *)serialized.data();
|
||||
bp3->type = 0;
|
||||
bp3->reserved = 0;
|
||||
bp3->payload_size = htons(packet->payload.size());
|
||||
memcpy(bp3->payload, packet->payload.data(), packet->payload.size());
|
||||
|
||||
return websocket_->Send(serialized.data(), serialized.size(), true);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
return websocket_->Send(packet->payload.data(), packet->payload.size(), true);
|
||||
}
|
||||
}
|
||||
|
||||
bool WebsocketProtocol::SendText(const std::string& text) {
|
||||
if (websocket_ == nullptr || !websocket_->IsConnected()) {
|
||||
bool WebsocketProtocol::SendText(const std::string &text)
|
||||
{
|
||||
if (websocket_ == nullptr || !websocket_->IsConnected())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!websocket_->Send(text)) {
|
||||
if (!websocket_->Send(text))
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to send text: %s", text.c_str());
|
||||
SetError(Lang::Strings::SERVER_ERROR);
|
||||
return false;
|
||||
@ -71,20 +84,24 @@ bool WebsocketProtocol::SendText(const std::string& text) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebsocketProtocol::IsAudioChannelOpened() const {
|
||||
bool WebsocketProtocol::IsAudioChannelOpened() const
|
||||
{
|
||||
return websocket_ != nullptr && websocket_->IsConnected() && !error_occurred_ && !IsTimeout();
|
||||
}
|
||||
|
||||
void WebsocketProtocol::CloseAudioChannel() {
|
||||
void WebsocketProtocol::CloseAudioChannel()
|
||||
{
|
||||
websocket_.reset();
|
||||
}
|
||||
|
||||
bool WebsocketProtocol::OpenAudioChannel() {
|
||||
bool WebsocketProtocol::OpenAudioChannel()
|
||||
{
|
||||
Settings settings("websocket", false);
|
||||
std::string url = settings.GetString("url");
|
||||
std::string token = settings.GetString("token");
|
||||
int version = settings.GetInt("version");
|
||||
if (version != 0) {
|
||||
if (version != 0)
|
||||
{
|
||||
version_ = version;
|
||||
}
|
||||
|
||||
@ -92,14 +109,17 @@ bool WebsocketProtocol::OpenAudioChannel() {
|
||||
|
||||
auto network = Board::GetInstance().GetNetwork();
|
||||
websocket_ = network->CreateWebSocket(1);
|
||||
if (websocket_ == nullptr) {
|
||||
if (websocket_ == nullptr)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to create websocket");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!token.empty()) {
|
||||
if (!token.empty())
|
||||
{
|
||||
// If token not has a space, add "Bearer " prefix
|
||||
if (token.find(" ") == std::string::npos) {
|
||||
if (token.find(" ") == std::string::npos)
|
||||
{
|
||||
token = "Bearer " + token;
|
||||
}
|
||||
websocket_->SetHeader("Authorization", token.c_str());
|
||||
@ -108,7 +128,8 @@ bool WebsocketProtocol::OpenAudioChannel() {
|
||||
websocket_->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
|
||||
websocket_->SetHeader("Client-Id", Board::GetInstance().GetUuid().c_str());
|
||||
|
||||
websocket_->OnData([this](const char* data, size_t len, bool binary) {
|
||||
websocket_->OnData([this](const char *data, size_t len, bool binary)
|
||||
{
|
||||
if (binary) {
|
||||
if (on_incoming_audio_ != nullptr) {
|
||||
if (version_ == 2) {
|
||||
@ -157,22 +178,35 @@ bool WebsocketProtocol::OpenAudioChannel() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check if it is a MCP message (JSON-RPC 2.0)
|
||||
auto jsonrpc = cJSON_GetObjectItem(root, "jsonrpc");
|
||||
if (cJSON_IsString(jsonrpc) && strcmp(jsonrpc->valuestring, "2.0") == 0) {
|
||||
if (on_incoming_json_ != nullptr) {
|
||||
// Wrap the MCP message in a "mcp" type message
|
||||
auto wrapper = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(wrapper, "type", "mcp");
|
||||
cJSON_AddItemToObject(wrapper, "payload", root);
|
||||
on_incoming_json_(wrapper);
|
||||
cJSON_Delete(wrapper);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGE(TAG, "Missing message type, data: %s", data);
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
last_incoming_time_ = std::chrono::steady_clock::now();
|
||||
});
|
||||
last_incoming_time_ = std::chrono::steady_clock::now(); });
|
||||
|
||||
websocket_->OnDisconnected([this]() {
|
||||
websocket_->OnDisconnected([this]()
|
||||
{
|
||||
ESP_LOGI(TAG, "Websocket disconnected");
|
||||
if (on_audio_channel_closed_ != nullptr) {
|
||||
on_audio_channel_closed_();
|
||||
}
|
||||
});
|
||||
} });
|
||||
|
||||
ESP_LOGI(TAG, "Connecting to websocket server: %s with version: %d", url.c_str(), version_);
|
||||
if (!websocket_->Connect(url.c_str())) {
|
||||
if (!websocket_->Connect(url.c_str()))
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to connect to websocket server");
|
||||
SetError(Lang::Strings::SERVER_NOT_CONNECTED);
|
||||
return false;
|
||||
@ -180,38 +214,42 @@ bool WebsocketProtocol::OpenAudioChannel() {
|
||||
|
||||
// Send hello message to describe the client
|
||||
auto message = GetHelloMessage();
|
||||
if (!SendText(message)) {
|
||||
if (!SendText(message))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for server hello
|
||||
EventBits_t bits = xEventGroupWaitBits(event_group_handle_, WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT, pdTRUE, pdFALSE, pdMS_TO_TICKS(10000));
|
||||
if (!(bits & WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT)) {
|
||||
if (!(bits & WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT))
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to receive server hello");
|
||||
SetError(Lang::Strings::SERVER_TIMEOUT);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (on_audio_channel_opened_ != nullptr) {
|
||||
if (on_audio_channel_opened_ != nullptr)
|
||||
{
|
||||
on_audio_channel_opened_();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string WebsocketProtocol::GetHelloMessage() {
|
||||
std::string WebsocketProtocol::GetHelloMessage()
|
||||
{
|
||||
// keys: message type, version, audio_params (format, sample_rate, channels)
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(root, "type", "hello");
|
||||
cJSON_AddNumberToObject(root, "version", version_);
|
||||
cJSON* features = cJSON_CreateObject();
|
||||
cJSON *features = cJSON_CreateObject();
|
||||
#if CONFIG_USE_SERVER_AEC
|
||||
cJSON_AddBoolToObject(features, "aec", true);
|
||||
#endif
|
||||
cJSON_AddBoolToObject(features, "mcp", true);
|
||||
cJSON_AddItemToObject(root, "features", features);
|
||||
cJSON_AddStringToObject(root, "transport", "websocket");
|
||||
cJSON* audio_params = cJSON_CreateObject();
|
||||
cJSON *audio_params = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(audio_params, "format", "opus");
|
||||
cJSON_AddNumberToObject(audio_params, "sample_rate", 16000);
|
||||
cJSON_AddNumberToObject(audio_params, "channels", 1);
|
||||
@ -224,27 +262,33 @@ std::string WebsocketProtocol::GetHelloMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
void WebsocketProtocol::ParseServerHello(const cJSON* root) {
|
||||
void WebsocketProtocol::ParseServerHello(const cJSON *root)
|
||||
{
|
||||
auto transport = cJSON_GetObjectItem(root, "transport");
|
||||
if (transport == nullptr || strcmp(transport->valuestring, "websocket") != 0) {
|
||||
if (transport == nullptr || strcmp(transport->valuestring, "websocket") != 0)
|
||||
{
|
||||
ESP_LOGE(TAG, "Unsupported transport: %s", transport->valuestring);
|
||||
return;
|
||||
}
|
||||
|
||||
auto session_id = cJSON_GetObjectItem(root, "session_id");
|
||||
if (cJSON_IsString(session_id)) {
|
||||
if (cJSON_IsString(session_id))
|
||||
{
|
||||
session_id_ = session_id->valuestring;
|
||||
ESP_LOGI(TAG, "Session ID: %s", session_id_.c_str());
|
||||
}
|
||||
|
||||
auto audio_params = cJSON_GetObjectItem(root, "audio_params");
|
||||
if (cJSON_IsObject(audio_params)) {
|
||||
if (cJSON_IsObject(audio_params))
|
||||
{
|
||||
auto sample_rate = cJSON_GetObjectItem(audio_params, "sample_rate");
|
||||
if (cJSON_IsNumber(sample_rate)) {
|
||||
if (cJSON_IsNumber(sample_rate))
|
||||
{
|
||||
server_sample_rate_ = sample_rate->valueint;
|
||||
}
|
||||
auto frame_duration = cJSON_GetObjectItem(audio_params, "frame_duration");
|
||||
if (cJSON_IsNumber(frame_duration)) {
|
||||
if (cJSON_IsNumber(frame_duration))
|
||||
{
|
||||
server_frame_duration_ = frame_duration->valueint;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user