add update with custom ota url - storage on aws

This commit is contained in:
MinhQuan7 2026-01-09 20:35:22 +07:00
parent 1b632ebd63
commit 86a0314a87
12 changed files with 851 additions and 252 deletions

View File

@ -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

View File

@ -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");

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -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

View File

@ -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

View File

@ -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);
};

View File

@ -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);

View File

@ -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)

View File

@ -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;
}
}