feat: Add camera functions (lichuang-dev)

This commit is contained in:
Terrence
2025-05-25 17:07:07 +08:00
parent ecfebc4a29
commit 249d12ac25
12 changed files with 472 additions and 72 deletions

View File

@@ -4,7 +4,7 @@
# CMakeLists in this exact order for cmake to work correctly # CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
set(PROJECT_VER "1.6.3") set(PROJECT_VER "1.6.4")
# Add this line to disable the specific warning # Add this line to disable the specific warning
add_compile_options(-Wno-missing-field-initializers) add_compile_options(-Wno-missing-field-initializers)

View File

@@ -57,6 +57,10 @@ Display* Board::GetDisplay() {
return &display; return &display;
} }
Camera* Board::GetCamera() {
return nullptr;
}
Led* Board::GetLed() { Led* Board::GetLed() {
static NoLed led; static NoLed led;
return &led; return &led;

View File

@@ -9,6 +9,7 @@
#include "led/led.h" #include "led/led.h"
#include "backlight.h" #include "backlight.h"
#include "camera.h"
void* create_board(); void* create_board();
class AudioCodec; class AudioCodec;
@@ -39,6 +40,7 @@ public:
virtual AudioCodec* GetAudioCodec() = 0; virtual AudioCodec* GetAudioCodec() = 0;
virtual bool GetTemperature(float& esp32temp); virtual bool GetTemperature(float& esp32temp);
virtual Display* GetDisplay(); virtual Display* GetDisplay();
virtual Camera* GetCamera();
virtual Http* CreateHttp() = 0; virtual Http* CreateHttp() = 0;
virtual WebSocket* CreateWebSocket() = 0; virtual WebSocket* CreateWebSocket() = 0;
virtual Mqtt* CreateMqtt() = 0; virtual Mqtt* CreateMqtt() = 0;

View File

@@ -0,0 +1,13 @@
#ifndef CAMERA_H
#define CAMERA_H
#include <string>
class Camera {
public:
virtual void SetExplainUrl(const std::string& url, const std::string& token) = 0;
virtual bool Capture() = 0;
virtual std::string Explain(const std::string& question) = 0;
};
#endif // CAMERA_H

View File

@@ -0,0 +1,212 @@
#include "esp32_camera.h"
#include "mcp_server.h"
#include "display.h"
#include "board.h"
#include <esp_log.h>
#include <esp_heap_caps.h>
#include <img_converters.h>
#include <cstring>
#define TAG "Esp32Camera"
Esp32Camera::Esp32Camera(const camera_config_t& config) {
jpeg_queue_ = xQueueCreate(10, sizeof(JpegChunk));
if (jpeg_queue_ == nullptr) {
ESP_LOGE(TAG, "Failed to create JPEG queue");
return;
}
// camera init
esp_err_t err = esp_camera_init(&config); // 配置上面定义的参数
if (err != ESP_OK) {
ESP_LOGE(TAG, "Camera init failed with error 0x%x", err);
return;
}
sensor_t *s = esp_camera_sensor_get(); // 获取摄像头型号
if (s->id.PID == GC0308_PID) {
s->set_hmirror(s, 0); // 这里控制摄像头镜像 写1镜像 写0不镜像
}
// 初始化预览图片的内存
memset(&preview_image_, 0, sizeof(preview_image_));
preview_image_.header.magic = LV_IMAGE_HEADER_MAGIC;
preview_image_.header.cf = LV_COLOR_FORMAT_RGB565;
preview_image_.header.flags = LV_IMAGE_FLAGS_ALLOCATED | LV_IMAGE_FLAGS_MODIFIABLE;
if (config.frame_size == FRAMESIZE_VGA) {
preview_image_.header.w = 640;
preview_image_.header.h = 480;
} else if (config.frame_size == FRAMESIZE_QVGA) {
preview_image_.header.w = 320;
preview_image_.header.h = 240;
}
preview_image_.header.stride = preview_image_.header.w * 2;
preview_image_.data_size = preview_image_.header.w * preview_image_.header.h * 2;
preview_image_.data = (uint8_t*)heap_caps_malloc(preview_image_.data_size, MALLOC_CAP_SPIRAM);
if (preview_image_.data == nullptr) {
ESP_LOGE(TAG, "Failed to allocate memory for preview image");
return;
}
}
Esp32Camera::~Esp32Camera() {
if (fb_) {
esp_camera_fb_return(fb_);
fb_ = nullptr;
}
if (preview_image_.data) {
heap_caps_free((void*)preview_image_.data);
preview_image_.data = nullptr;
}
esp_camera_deinit();
if (jpeg_queue_ != nullptr) {
vQueueDelete(jpeg_queue_);
jpeg_queue_ = nullptr;
}
}
void Esp32Camera::SetExplainUrl(const std::string& url, const std::string& token) {
explain_url_ = url;
explain_token_ = token;
}
bool Esp32Camera::Capture() {
if (preview_thread_.joinable()) {
preview_thread_.join();
}
if (encoder_thread_.joinable()) {
encoder_thread_.join();
}
int frames_to_get = 2;
// Try to get a stable frame
for (int i = 0; i < frames_to_get; i++) {
if (fb_ != nullptr) {
esp_camera_fb_return(fb_);
}
fb_ = esp_camera_fb_get();
if (fb_ == nullptr) {
ESP_LOGE(TAG, "Camera capture failed");
return false;
}
}
preview_thread_ = std::thread([this]() {
// 显示预览图片
auto display = Board::GetInstance().GetDisplay();
if (display != nullptr) {
auto src = (uint16_t*)fb_->buf;
auto dst = (uint16_t*)preview_image_.data;
size_t pixel_count = fb_->len / 2;
for (size_t i = 0; i < pixel_count; i++) {
// 交换每个16位字内的字节
dst[i] = __builtin_bswap16(src[i]);
}
display->SetPreviewImage(&preview_image_);
}
});
return true;
}
std::string Esp32Camera::Explain(const std::string& question) {
if (explain_url_.empty() || explain_token_.empty()) {
return "{\"success\": false, \"message\": \"Image explain URL or token is not set\"}";
}
// We spawn a thread to encode the image to JPEG
encoder_thread_ = std::thread([this]() {
frame2jpg_cb(fb_, 80, [](void* arg, size_t index, const void* data, size_t len) -> unsigned int {
auto jpeg_queue = (QueueHandle_t)arg;
JpegChunk chunk = {
.data = (uint8_t*)heap_caps_malloc(len, MALLOC_CAP_SPIRAM),
.len = len
};
memcpy(chunk.data, data, len);
xQueueSend(jpeg_queue, &chunk, portMAX_DELAY);
return len;
}, jpeg_queue_);
});
auto http = Board::GetInstance().CreateHttp();
// 构造multipart/form-data请求体
std::string boundary = "----ESP32_CAMERA_BOUNDARY";
// 构造question字段
std::string question_field;
question_field += "--" + boundary + "\r\n";
question_field += "Content-Disposition: form-data; name=\"question\"\r\n";
question_field += "\r\n";
question_field += question + "\r\n";
// 构造文件字段头部
std::string file_header;
file_header += "--" + boundary + "\r\n";
file_header += "Content-Disposition: form-data; name=\"file\"; filename=\"camera.jpg\"\r\n";
file_header += "Content-Type: image/jpeg\r\n";
file_header += "\r\n";
// 构造尾部
std::string multipart_footer;
multipart_footer += "\r\n--" + boundary + "--\r\n";
// 配置HTTP客户端使用分块传输编码
http->SetHeader("Authorization", "Bearer " + explain_token_);
http->SetHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
http->SetHeader("Transfer-Encoding", "chunked");
if (!http->Open("POST", explain_url_)) {
ESP_LOGE(TAG, "Failed to connect to explain URL");
// Clear the queue
encoder_thread_.join();
JpegChunk chunk;
while (xQueueReceive(jpeg_queue_, &chunk, portMAX_DELAY) == pdPASS) {
if (chunk.data != nullptr) {
heap_caps_free(chunk.data);
} else {
break;
}
}
return "{\"success\": false, \"message\": \"Failed to connect to explain URL\"}";
}
// 第一块question字段
http->Write(question_field.c_str(), question_field.size());
// 第二块:文件字段头部
http->Write(file_header.c_str(), file_header.size());
// 第三块JPEG数据
size_t total_sent = 0;
while (true) {
JpegChunk chunk;
if (xQueueReceive(jpeg_queue_, &chunk, portMAX_DELAY) != pdPASS) {
ESP_LOGE(TAG, "Failed to receive JPEG chunk");
break;
}
if (chunk.data == nullptr) {
break; // The last chunk
}
http->Write((const char*)chunk.data, chunk.len);
total_sent += chunk.len;
heap_caps_free(chunk.data);
}
// 第四块multipart尾部
http->Write(multipart_footer.c_str(), multipart_footer.size());
// 结束块
http->Write("", 0);
if (http->GetStatusCode() != 200) {
ESP_LOGE(TAG, "Failed to upload photo, status code: %d", http->GetStatusCode());
return "{\"success\": false, \"message\": \"Failed to upload photo\"}";
}
std::string result = http->ReadAll();
http->Close();
ESP_LOGI(TAG, "Explain image size=%dx%d, compressed size=%d, question=%s\n%s", fb_->width, fb_->height, total_sent, question.c_str(), result.c_str());
return result;
}

View File

@@ -0,0 +1,38 @@
#ifndef ESP32_CAMERA_H
#define ESP32_CAMERA_H
#include <esp_camera.h>
#include <lvgl.h>
#include <thread>
#include <memory>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include "camera.h"
struct JpegChunk {
uint8_t* data;
size_t len;
};
class Esp32Camera : public Camera {
private:
camera_fb_t* fb_ = nullptr;
lv_img_dsc_t preview_image_;
std::string explain_url_;
std::string explain_token_;
std::thread preview_thread_;
std::thread encoder_thread_;
QueueHandle_t jpeg_queue_ = nullptr;
public:
Esp32Camera(const camera_config_t& config);
~Esp32Camera();
virtual void SetExplainUrl(const std::string& url, const std::string& token);
virtual bool Capture();
virtual std::string Explain(const std::string& question);
};
#endif // ESP32_CAMERA_H

View File

@@ -37,5 +37,26 @@
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_42 #define DISPLAY_BACKLIGHT_PIN GPIO_NUM_42
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true #define DISPLAY_BACKLIGHT_OUTPUT_INVERT true
/* Camera pins */
#define CAMERA_PIN_PWDN -1
#define CAMERA_PIN_RESET -1
#define CAMERA_PIN_XCLK 5
#define CAMERA_PIN_SIOD 1
#define CAMERA_PIN_SIOC 2
#define CAMERA_PIN_D7 9
#define CAMERA_PIN_D6 4
#define CAMERA_PIN_D5 6
#define CAMERA_PIN_D4 15
#define CAMERA_PIN_D3 17
#define CAMERA_PIN_D2 8
#define CAMERA_PIN_D1 18
#define CAMERA_PIN_D0 16
#define CAMERA_PIN_VSYNC 3
#define CAMERA_PIN_HREF 46
#define CAMERA_PIN_PCLK 7
#define XCLK_FREQ_HZ 24000000
#endif // _BOARD_CONFIG_H_ #endif // _BOARD_CONFIG_H_

View File

@@ -6,6 +6,7 @@
#include "config.h" #include "config.h"
#include "i2c_device.h" #include "i2c_device.h"
#include "iot/thing_manager.h" #include "iot/thing_manager.h"
#include "esp32_camera.h"
#include <esp_log.h> #include <esp_log.h>
#include <esp_lcd_panel_vendor.h> #include <esp_lcd_panel_vendor.h>
@@ -36,6 +37,36 @@ public:
} }
}; };
class CustomAudioCodec : public BoxAudioCodec {
private:
Pca9557* pca9557_;
public:
CustomAudioCodec(i2c_master_bus_handle_t i2c_bus, Pca9557* pca9557)
: BoxAudioCodec(i2c_bus,
AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK,
AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN,
GPIO_NUM_NC,
AUDIO_CODEC_ES8311_ADDR,
AUDIO_CODEC_ES7210_ADDR,
AUDIO_INPUT_REFERENCE),
pca9557_(pca9557) {
}
virtual void EnableOutput(bool enable) override {
BoxAudioCodec::EnableOutput(enable);
if (enable) {
pca9557_->SetOutputState(1, 1);
} else {
pca9557_->SetOutputState(1, 0);
}
}
};
class LichuangDevBoard : public WifiBoard { class LichuangDevBoard : public WifiBoard {
private: private:
@@ -44,6 +75,7 @@ private:
Button boot_button_; Button boot_button_;
LcdDisplay* display_; LcdDisplay* display_;
Pca9557* pca9557_; Pca9557* pca9557_;
Esp32Camera* camera_;
void InitializeI2c() { void InitializeI2c() {
// Initialize I2C peripheral // Initialize I2C peripheral
@@ -164,11 +196,39 @@ private:
lvgl_port_add_touch(&touch_cfg); lvgl_port_add_touch(&touch_cfg);
} }
// 物联网初始化,添加对 AI 可见设备 void InitializeCamera() {
void InitializeIot() { // Open camera power
auto& thing_manager = iot::ThingManager::GetInstance(); pca9557_->SetOutputState(2, 0);
thing_manager.AddThing(iot::CreateThing("Speaker"));
thing_manager.AddThing(iot::CreateThing("Screen")); camera_config_t config = {};
config.ledc_channel = LEDC_CHANNEL_2; // LEDC通道选择 用于生成XCLK时钟 但是S3不用
config.ledc_timer = LEDC_TIMER_2; // LEDC timer选择 用于生成XCLK时钟 但是S3不用
config.pin_d0 = CAMERA_PIN_D0;
config.pin_d1 = CAMERA_PIN_D1;
config.pin_d2 = CAMERA_PIN_D2;
config.pin_d3 = CAMERA_PIN_D3;
config.pin_d4 = CAMERA_PIN_D4;
config.pin_d5 = CAMERA_PIN_D5;
config.pin_d6 = CAMERA_PIN_D6;
config.pin_d7 = CAMERA_PIN_D7;
config.pin_xclk = CAMERA_PIN_XCLK;
config.pin_pclk = CAMERA_PIN_PCLK;
config.pin_vsync = CAMERA_PIN_VSYNC;
config.pin_href = CAMERA_PIN_HREF;
config.pin_sccb_sda = -1; // 这里写-1 表示使用已经初始化的I2C接口
config.pin_sccb_scl = CAMERA_PIN_SIOC;
config.sccb_i2c_port = 1;
config.pin_pwdn = CAMERA_PIN_PWDN;
config.pin_reset = CAMERA_PIN_RESET;
config.xclk_freq_hz = XCLK_FREQ_HZ;
config.pixel_format = PIXFORMAT_RGB565;
config.frame_size = FRAMESIZE_VGA;
config.jpeg_quality = 12;
config.fb_count = 1;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
camera_ = new Esp32Camera(config);
} }
public: public:
@@ -178,24 +238,20 @@ public:
InitializeSt7789Display(); InitializeSt7789Display();
InitializeTouch(); InitializeTouch();
InitializeButtons(); InitializeButtons();
InitializeIot(); InitializeCamera();
#if CONFIG_IOT_PROTOCOL_XIAOZHI
auto& thing_manager = iot::ThingManager::GetInstance();
thing_manager.AddThing(iot::CreateThing("Speaker"));
thing_manager.AddThing(iot::CreateThing("Screen"));
#endif
GetBacklight()->RestoreBrightness(); GetBacklight()->RestoreBrightness();
} }
virtual AudioCodec* GetAudioCodec() override { virtual AudioCodec* GetAudioCodec() override {
static BoxAudioCodec audio_codec( static CustomAudioCodec audio_codec(
i2c_bus_, i2c_bus_,
AUDIO_INPUT_SAMPLE_RATE, pca9557_);
AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK,
AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN,
GPIO_NUM_NC,
AUDIO_CODEC_ES8311_ADDR,
AUDIO_CODEC_ES7210_ADDR,
AUDIO_INPUT_REFERENCE);
return &audio_codec; return &audio_codec;
} }
@@ -207,6 +263,10 @@ public:
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight; return &backlight;
} }
virtual Camera* GetCamera() override {
return camera_;
}
}; };
DECLARE_BOARD(LichuangDevBoard); DECLARE_BOARD(LichuangDevBoard);

View File

@@ -11,13 +11,14 @@ dependencies:
78/esp_lcd_nv3023: ~1.0.0 78/esp_lcd_nv3023: ~1.0.0
78/esp-wifi-connect: ~2.4.2 78/esp-wifi-connect: ~2.4.2
78/esp-opus-encoder: ~2.3.2 78/esp-opus-encoder: ~2.3.2
78/esp-ml307: ~2.0.3 78/esp-ml307: ~2.1.0
78/xiaozhi-fonts: ~1.3.2 78/xiaozhi-fonts: ~1.3.2
espressif/led_strip: ^2.5.5 espressif/led_strip: ^2.5.5
espressif/esp_codec_dev: ~1.3.2 espressif/esp_codec_dev: ~1.3.2
espressif/esp-sr: ~2.1.1 espressif/esp-sr: ~2.1.1
espressif/button: ~4.1.3 espressif/button: ~4.1.3
espressif/knob: ^1.0.0 espressif/knob: ^1.0.0
espressif/esp32-camera: ^2.0.15
espressif/esp_lcd_touch_ft5x06: ~1.0.7 espressif/esp_lcd_touch_ft5x06: ~1.0.7
espressif/esp_lcd_touch_gt911: ^1 espressif/esp_lcd_touch_gt911: ^1
waveshare/esp_lcd_touch_cst9217: ^1.0.3 waveshare/esp_lcd_touch_cst9217: ^1.0.3
@@ -41,7 +42,7 @@ dependencies:
version: '*' version: '*'
rules: rules:
- if: target in [esp32p4] - if: target in [esp32p4]
## Required IDF version ## Required IDF version
idf: idf:
version: '>=5.3' version: '>=5.3'

View File

@@ -19,15 +19,24 @@ McpServer::McpServer() {
AddCommonTools(); AddCommonTools();
} }
McpServer::~McpServer() {
for (auto tool : tools_) {
delete tool;
}
tools_.clear();
}
void McpServer::AddCommonTools() { void McpServer::AddCommonTools() {
auto& board = Board::GetInstance();
AddTool("self.get_device_status", AddTool("self.get_device_status",
"Provides the real-time information of the device, including the current status of the audio speaker, screen, battery, network, etc.\n" "Provides the real-time information of the device, including the current status of the audio speaker, screen, battery, network, etc.\n"
"Use this tool for: \n" "Use this tool for: \n"
"1. Answering questions about current condition (e.g. what is the current volume of the audio speaker?)\n" "1. Answering questions about current condition (e.g. what is the current volume of the audio speaker?)\n"
"2. As the first step to control the device (e.g. turn up / down the volume of the audio speaker, etc.)", "2. As the first step to control the device (e.g. turn up / down the volume of the audio speaker, etc.)",
PropertyList(), PropertyList(),
[](const PropertyList& properties) -> ReturnValue { [&board](const PropertyList& properties) -> ReturnValue {
return Board::GetInstance().GetDeviceStatusJson(); return board.GetDeviceStatusJson();
}); });
AddTool("self.audio_speaker.set_volume", AddTool("self.audio_speaker.set_volume",
@@ -35,47 +44,67 @@ void McpServer::AddCommonTools() {
PropertyList({ PropertyList({
Property("volume", kPropertyTypeInteger, 0, 100) Property("volume", kPropertyTypeInteger, 0, 100)
}), }),
[](const PropertyList& properties) -> ReturnValue { [&board](const PropertyList& properties) -> ReturnValue {
auto codec = Board::GetInstance().GetAudioCodec(); auto codec = board.GetAudioCodec();
codec->SetOutputVolume(properties["volume"].value<int>()); codec->SetOutputVolume(properties["volume"].value<int>());
return true; return true;
}); });
AddTool("self.screen.set_brightness", auto backlight = board.GetBacklight();
"Set the brightness of the screen.", if (backlight) {
PropertyList({ AddTool("self.screen.set_brightness",
Property("brightness", kPropertyTypeInteger, 0, 100) "Set the brightness of the screen.",
}), PropertyList({
[](const PropertyList& properties) -> ReturnValue { Property("brightness", kPropertyTypeInteger, 0, 100)
uint8_t brightness = static_cast<uint8_t>(properties["brightness"].value<int>()); }),
auto backlight = Board::GetInstance().GetBacklight(); [backlight](const PropertyList& properties) -> ReturnValue {
if (backlight) { uint8_t brightness = static_cast<uint8_t>(properties["brightness"].value<int>());
backlight->SetBrightness(brightness, true); backlight->SetBrightness(brightness, true);
} return true;
return true; });
}); }
AddTool("self.screen.set_theme", auto display = board.GetDisplay();
"Set the theme of the screen. The theme can be 'light' or 'dark'.", if (display && !display->GetTheme().empty()) {
PropertyList({ AddTool("self.screen.set_theme",
Property("theme", kPropertyTypeString) "Set the theme of the screen. The theme can be `light` or `dark`.",
}), PropertyList({
[](const PropertyList& properties) -> ReturnValue { Property("theme", kPropertyTypeString)
auto display = Board::GetInstance().GetDisplay(); }),
if (display) { [display](const PropertyList& properties) -> ReturnValue {
display->SetTheme(properties["theme"].value<std::string>().c_str()); display->SetTheme(properties["theme"].value<std::string>().c_str());
} return true;
return true; });
}); }
auto camera = board.GetCamera();
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"
"Args:\n"
" `question`: The question that you want to ask about the photo.\n"
"Return:\n"
" A JSON object that provides the photo information.",
PropertyList({
Property("question", kPropertyTypeString)
}),
[camera](const PropertyList& properties) -> ReturnValue {
if (!camera->Capture()) {
return "{\"success\": false, \"message\": \"Failed to capture photo\"}";
}
auto question = properties["question"].value<std::string>();
return camera->Explain(question);
});
}
} }
void McpServer::AddTool(McpTool* tool) { void McpServer::AddTool(McpTool* tool) {
ESP_LOGI(TAG, "Add tool: %s", tool->name().c_str());
tools_.push_back(tool); tools_.push_back(tool);
} }
void McpServer::AddTool(const std::string& name, const std::string& description, const PropertyList& properties, std::function<ReturnValue(const PropertyList&)> callback) { void McpServer::AddTool(const std::string& name, const std::string& description, const PropertyList& properties, std::function<ReturnValue(const PropertyList&)> callback) {
tools_.push_back(new McpTool(name, description, properties, callback)); AddTool(new McpTool(name, description, properties, callback));
} }
void McpServer::ParseMessage(const std::string& message) { void McpServer::ParseMessage(const std::string& message) {
@@ -88,6 +117,21 @@ void McpServer::ParseMessage(const std::string& message) {
cJSON_Delete(json); cJSON_Delete(json);
} }
void McpServer::ParseCapabilities(const cJSON* capabilities) {
auto vision = cJSON_GetObjectItem(capabilities, "vision");
if (cJSON_IsObject(vision)) {
auto url = cJSON_GetObjectItem(vision, "url");
auto token = cJSON_GetObjectItem(vision, "token");
if (cJSON_IsString(url) && cJSON_IsString(token)) {
ESP_LOGI(TAG, "Setting explain URL: %s, token: %s", url->valuestring, token->valuestring);
auto camera = Board::GetInstance().GetCamera();
if (camera) {
camera->SetExplainUrl(std::string(url->valuestring), std::string(token->valuestring));
}
}
}
}
void McpServer::ParseMessage(const cJSON* json) { void McpServer::ParseMessage(const cJSON* json) {
// Check JSONRPC version // Check JSONRPC version
auto version = cJSON_GetObjectItem(json, "jsonrpc"); auto version = cJSON_GetObjectItem(json, "jsonrpc");
@@ -123,6 +167,12 @@ void McpServer::ParseMessage(const cJSON* json) {
auto id_int = id->valueint; auto id_int = id->valueint;
if (method_str == "initialize") { if (method_str == "initialize") {
if (cJSON_IsObject(params)) {
auto capabilities = cJSON_GetObjectItem(params, "capabilities");
if (cJSON_IsObject(capabilities)) {
ParseCapabilities(capabilities);
}
}
auto app_desc = esp_app_get_description(); auto app_desc = esp_app_get_description();
std::string message = "{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{}},\"serverInfo\":{\"name\":\"" BOARD_NAME "\",\"version\":\""; std::string message = "{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{}},\"serverInfo\":{\"name\":\"" BOARD_NAME "\",\"version\":\"";
message += app_desc->version; message += app_desc->version;

View File

@@ -262,9 +262,10 @@ public:
private: private:
McpServer(); McpServer();
~McpServer() = default; ~McpServer();
void AddCommonTools(); void AddCommonTools();
void ParseCapabilities(const cJSON* capabilities);
void ReplyResult(int id, const std::string& result); void ReplyResult(int id, const std::string& result);
void ReplyError(int id, const std::string& message); void ReplyError(int id, const std::string& message);

View File

@@ -84,18 +84,25 @@ bool Ota::CheckVersion() {
return false; return false;
} }
auto http = SetupHttp(); auto http = std::unique_ptr<Http>(SetupHttp());
std::string data = board.GetJson(); std::string data = board.GetJson();
std::string method = data.length() > 0 ? "POST" : "GET"; std::string method = data.length() > 0 ? "POST" : "GET";
if (!http->Open(method, check_version_url_, data)) { http->SetContent(std::move(data));
if (!http->Open(method, check_version_url_)) {
ESP_LOGE(TAG, "Failed to open HTTP connection"); ESP_LOGE(TAG, "Failed to open HTTP connection");
delete http;
return false; return false;
} }
data = http->GetBody(); auto status_code = http->GetStatusCode();
delete http; if (status_code != 200) {
ESP_LOGE(TAG, "Failed to check version, status code: %d", status_code);
return false;
}
data = http->ReadAll();
http->Close();
// Response: { "firmware": { "version": "1.0.0", "url": "http://" } } // Response: { "firmware": { "version": "1.0.0", "url": "http://" } }
// Parse the JSON response and check if the version is newer // Parse the JSON response and check if the version is newer
@@ -257,17 +264,15 @@ void Ota::Upgrade(const std::string& firmware_url) {
bool image_header_checked = false; bool image_header_checked = false;
std::string image_header; std::string image_header;
auto http = Board::GetInstance().CreateHttp(); auto http = std::unique_ptr<Http>(Board::GetInstance().CreateHttp());
if (!http->Open("GET", firmware_url)) { if (!http->Open("GET", firmware_url)) {
ESP_LOGE(TAG, "Failed to open HTTP connection"); ESP_LOGE(TAG, "Failed to open HTTP connection");
delete http;
return; return;
} }
size_t content_length = http->GetBodyLength(); size_t content_length = http->GetBodyLength();
if (content_length == 0) { if (content_length == 0) {
ESP_LOGE(TAG, "Failed to get content length"); ESP_LOGE(TAG, "Failed to get content length");
delete http;
return; return;
} }
@@ -278,7 +283,6 @@ void Ota::Upgrade(const std::string& firmware_url) {
int ret = http->Read(buffer, sizeof(buffer)); int ret = http->Read(buffer, sizeof(buffer));
if (ret < 0) { if (ret < 0) {
ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret)); ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret));
delete http;
return; return;
} }
@@ -309,13 +313,11 @@ void Ota::Upgrade(const std::string& firmware_url) {
auto current_version = esp_app_get_description()->version; auto current_version = esp_app_get_description()->version;
if (memcmp(new_app_info.version, current_version, sizeof(new_app_info.version)) == 0) { if (memcmp(new_app_info.version, current_version, sizeof(new_app_info.version)) == 0) {
ESP_LOGE(TAG, "Firmware version is the same, skipping upgrade"); ESP_LOGE(TAG, "Firmware version is the same, skipping upgrade");
delete http;
return; return;
} }
if (esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle)) { if (esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle)) {
esp_ota_abort(update_handle); esp_ota_abort(update_handle);
delete http;
ESP_LOGE(TAG, "Failed to begin OTA"); ESP_LOGE(TAG, "Failed to begin OTA");
return; return;
} }
@@ -328,11 +330,10 @@ void Ota::Upgrade(const std::string& firmware_url) {
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_ota_abort(update_handle); esp_ota_abort(update_handle);
delete http;
return; return;
} }
} }
delete http; http->Close();
esp_err_t err = esp_ota_end(update_handle); esp_err_t err = esp_ota_end(update_handle);
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -437,25 +438,22 @@ esp_err_t Ota::Activate() {
url += "activate"; url += "activate";
} }
auto http = SetupHttp(); auto http = std::unique_ptr<Http>(SetupHttp());
std::string data = GetActivationPayload(); std::string data = GetActivationPayload();
if (!http->Open("POST", url, data)) { http->SetContent(std::move(data));
if (!http->Open("POST", url)) {
ESP_LOGE(TAG, "Failed to open HTTP connection"); ESP_LOGE(TAG, "Failed to open HTTP connection");
delete http;
return ESP_FAIL; return ESP_FAIL;
} }
auto status_code = http->GetStatusCode(); auto status_code = http->GetStatusCode();
data = http->GetBody();
http->Close();
delete http;
if (status_code == 202) { if (status_code == 202) {
return ESP_ERR_TIMEOUT; return ESP_ERR_TIMEOUT;
} }
if (status_code != 200) { if (status_code != 200) {
ESP_LOGE(TAG, "Failed to activate, code: %d, body: %s", status_code, data.c_str()); ESP_LOGE(TAG, "Failed to activate, code: %d, body: %s", status_code, http->ReadAll().c_str());
return ESP_FAIL; return ESP_FAIL;
} }