云天 发表于 2025-9-4 20:26:48

行空板K10打造智能舵机云台与拍照识别系统

本帖最后由 云天 于 2025-9-4 20:28 编辑

在当今这个科技飞速发展的时代,智能硬件项目层出不穷,它们不仅丰富了我们的生活,还激发了无数创客的创造力。今天,我将为大家介绍一个基于行空板 K10 的有趣项目——智能舵机云台与拍照识别系统。这个项目结合了物联网、舵机控制和图像识别技术,通过通俗的语言指令就能控制云台的方向,并且能够拍照识别图片内容,非常适合有一定基础的创客朋友们来尝试。

【项目背景】

行空板 K10 是一款功能强大的开源硬件开发板,它集成了多种传感器和接口,能够方便地实现各种智能应用。而小智 AI 开源代码则为硬件设备提供了智能语音交互的能力。结合这两者,我们可以在行空板 K10 上实现一个智能舵机云台,通过语音指令控制云台的水平和垂直方向,并且利用其搭载的摄像头进行拍照和图像识别,这在智能家居、智能安防等领域有着广泛的应用前景。

【硬件准备】

1. **行空板 K10**:作为整个项目的控制核心,它将负责处理语音指令、控制舵机以及处理图像数据。
2. **SG90 舵机 × 2**:一个用于控制水平方向,另一个用于控制垂直方向。它们通过 PWM 信号接收控制指令,能够精确地调整角度。
3. **摄像头模块**:行空板 K10 自带的摄像头接口可以连接合适的摄像头模块,用于拍照和图像识别。
4. **连接线**:用于连接舵机和行空板 K10 的引脚,确保信号和电源的稳定传输。

【软件环境搭建】


[*]VS Code:作为代码编辑和开发的环境,它轻量且功能强大,支持 ESP-IDF 插件,方便我们进行 ESP32 系列芯片的开发。
[*]ESP-IDF:Espressif 提供的开发框架,用于开发基于 ESP32 系列芯片的项目。我们需要安装 ESP-IDF 并配置好相关环境,以便编译和上传代码到行空板 K10。
[*]小智 AI 开源代码:从其官方仓库获取最新的代码,它将为我们的项目提供语音识别和自然语言处理的功能。
[*]Mcp 服务器:Mcp 是一个轻量级的物联网通信协议,用于设备之间的通信和控制。使我们能够通过 Mcp 服务器实现语音指令控制舵机云台、拍照与图像识别功能。
(可参考:基于 ESP-IDF 和行空板 K10 的智能风扇与 LED 灯带控制系统)
【舵机控制实现】

在本项目中,我们使用了 `servo_controller.h` 和 `servo_controller.cc` 文件来实现舵机的控制逻辑。这两个文件封装了舵机的基本操作,包括设置角度、旋转、扫描等功能。通过创建 `ServoController` 类的实例,并指定舵机连接的引脚(P0 和 P1),我们就可以轻松地控制两个舵机的运动。

在代码中,我们首先初始化两个舵机控制器,分别对应水平和垂直方向的舵机。然后,通过小智 AI 的语音识别功能,解析用户的语音指令。例如,当用户说“向左转 30 度”时,系统会将这个指令转换为对应的舵机控制命令,调用 `RotateCounterclockwise` 方法,让水平方向的舵机逆时针旋转 30 度。
`servo_controller.cc` 代码:
#include "servo_controller.h"
#include <esp_log.h>
#include <cmath>

#define TAG4 "ServoController"

ServoController::ServoController(gpio_num_t servo_pin)
    : servo_pin_(servo_pin)
    , ledc_channel_(LEDC_CHANNEL_0)// 默认通道 0
    , ledc_timer_(LEDC_TIMER_0)      // 默认定时器 0
    , current_angle_(SERVO_DEFAULT_ANGLE)
    , target_angle_(SERVO_DEFAULT_ANGLE)
    , is_moving_(false)
    , is_sweeping_(false)
    , stop_requested_(false)
    , servo_task_handle_(nullptr)
    , command_queue_(nullptr)
    , on_move_complete_callback_(nullptr) {
   
    // 根据 GPIO 引脚分配不同的 LEDC 通道和定时器
    if (servo_pin == SERVO_PIN_HORIZONTAL) {
      ledc_channel_ = LEDC_CHANNEL_0;
      ledc_timer_ = LEDC_TIMER_0;
    } else if (servo_pin == SERVO_PIN_VERTICAL) {
      ledc_channel_ = LEDC_CHANNEL_1;
      ledc_timer_ = LEDC_TIMER_1;
    } else {
      ESP_LOGE(TAG4, "未知的舵机引脚: %d", servo_pin_);
    }
}

ServoController::~ServoController() {
    Stop();
    if (servo_task_handle_ != nullptr) {
      vTaskDelete(servo_task_handle_);
    }
    if (command_queue_ != nullptr) {
      vQueueDelete(command_queue_);
    }
}

bool ServoController::Initialize() {
    ESP_LOGI(TAG4, "初始化SG90舵机控制器,引脚: %d", servo_pin_);
   
    // 配置LEDC定时器 (ESP32-S3最大支持14位分辨率)
    ledc_timer_config_t timer_config = {
      .speed_mode = LEDC_LOW_SPEED_MODE,
      .duty_resolution = LEDC_TIMER_14_BIT,
      .timer_num = ledc_timer_,
      .freq_hz = 50, // 50Hz for servo
      .clk_cfg = LEDC_AUTO_CLK
    };
   
    esp_err_t ret = ledc_timer_config(&timer_config);
    if (ret != ESP_OK) {
      ESP_LOGE(TAG4, "LEDC定时器配置失败: %s", esp_err_to_name(ret));
      return false;
    }
   
    // 配置LEDC通道
    ledc_channel_config_t channel_config = {
      .gpio_num = servo_pin_,
      .speed_mode = LEDC_LOW_SPEED_MODE,
      .channel = ledc_channel_,
      .intr_type = LEDC_INTR_DISABLE,
      .timer_sel = ledc_timer_,
      .duty = 0,
      .hpoint = 0
    };
   
    ret = ledc_channel_config(&channel_config);
    if (ret != ESP_OK) {
      ESP_LOGE(TAG4, "LEDC通道配置失败: %s", esp_err_to_name(ret));
      return false;
    }
   
    // 创建命令队列
    command_queue_ = xQueueCreate(10, sizeof(ServoCommand));
    if (command_queue_ == nullptr) {
      ESP_LOGE(TAG4, "创建命令队列失败");
      return false;
    }
   
    // 创建舵机控制任务
    BaseType_t task_ret = xTaskCreate(
      ServoTask,
      "servo_task",
      4096,
      this,
      5,
      &servo_task_handle_
    );
   
    if (task_ret != pdPASS) {
      ESP_LOGE(TAG4, "创建舵机任务失败");
      return false;
    }
   
    // 设置初始位置
    WriteAngle(current_angle_);
   
    ESP_LOGI(TAG4, "SG90舵机控制器初始化成功");
    return true;
}
/*
void ServoController::InitializeTools() {
    auto& mcp_server = McpServer::GetInstance();
    ESP_LOGI(TAG4, "开始注册舵机MCP工具...");

    // 设置舵机角度
    mcp_server.AddTool("self.servo.set_angle",
                     "设置SG90舵机到指定角度。angle: 目标角度(0-180度)",
                     PropertyList({Property("angle", kPropertyTypeInteger, 90, 0, 180)}),
                     (const PropertyList& properties) -> ReturnValue {
                           int angle = properties["angle"].value<int>();
                           SetAngle(angle);
                           return "舵机设置到 " + std::to_string(angle) + " 度";
                     });

    // 顺时针旋转
    mcp_server.AddTool("self.servo.rotate_clockwise",
                     "顺时针旋转SG90舵机指定角度。degrees: 旋转角度(1-180度)",
                     PropertyList({Property("degrees", kPropertyTypeInteger, 30, 1, 180)}),
                     (const PropertyList& properties) -> ReturnValue {
                           int degrees = properties["degrees"].value<int>();
                           RotateClockwise(degrees);
                           return "舵机顺时针旋转 " + std::to_string(degrees) + " 度";
                     });

    // 逆时针旋转
    mcp_server.AddTool("self.servo.rotate_counterclockwise",
                     "逆时针旋转SG90舵机指定角度。degrees: 旋转角度(1-180度)",
                     PropertyList({Property("degrees", kPropertyTypeInteger, 30, 1, 180)}),
                     (const PropertyList& properties) -> ReturnValue {
                           int degrees = properties["degrees"].value<int>();
                           RotateCounterclockwise(degrees);
                           return "舵机逆时针旋转 " + std::to_string(degrees) + " 度";
                     });

    // 获取当前位置
    mcp_server.AddTool("self.servo.get_position",
                     "获取SG90舵机当前角度位置",
                     PropertyList(),
                     (const PropertyList& properties) -> ReturnValue {
                           int angle = GetCurrentAngle();
                           return "当前舵机角度: " + std::to_string(angle) + " 度";
                     });

    // 扫描模式
    mcp_server.AddTool("self.servo.sweep",
                     "SG90舵机扫描模式,在指定角度范围内来回摆动。"
                     "min_angle: 最小角度(0-179度); max_angle: 最大角度(1-180度); "
                     "speed: 摆动速度,毫秒(100-5000ms)",
                     PropertyList({Property("min_angle", kPropertyTypeInteger, 0, 0, 179),
                                     Property("max_angle", kPropertyTypeInteger, 180, 1, 180),
                                     Property("speed", kPropertyTypeInteger, 1000, 100, 5000)}),
                     (const PropertyList& properties) -> ReturnValue {
                           int min_angle = properties["min_angle"].value<int>();
                           int max_angle = properties["max_angle"].value<int>();
                           int speed = properties["speed"].value<int>();
                           SweepBetween(min_angle, max_angle, speed);
                           return "开始扫描模式: " + std::to_string(min_angle) + "° - " +
                                  std::to_string(max_angle) + "°";
                     });

    // 停止舵机
    mcp_server.AddTool("self.servo.stop",
                     "立即停止SG90舵机运动",
                     PropertyList(),
                     (const PropertyList& properties) -> ReturnValue {
                           Stop();
                           return "舵机已停止";
                     });

    // 复位到中心位置
    mcp_server.AddTool("self.servo.reset",
                     "将SG90舵机复位到中心位置(90度)",
                     PropertyList(),
                     (const PropertyList& properties) -> ReturnValue {
                           Reset();
                           return "舵机已复位到中心位置(90度)";
                     });

    // 获取舵机状态
    mcp_server.AddTool("self.servo.get_status",
                     "获取SG90舵机当前状态",
                     PropertyList(),
                     (const PropertyList& properties) -> ReturnValue {
                           int angle = GetCurrentAngle();
                           bool moving = IsMoving();
                           bool sweeping = IsSweeping();

                           std::string status = "{\"angle\":" + std::to_string(angle) +
                                              ",\"moving\":" + (moving ? "true" : "false") +
                                              ",\"sweeping\":" + (sweeping ? "true" : "false") + "}";
                           return status;
                     });

    ESP_LOGI(TAG4, "舵机MCP工具注册完成");
}
*/
void ServoController::SetAngle(int angle) {
    if (!IsValidAngle(angle)) {
      ESP_LOGW(TAG4, "无效角度: %d,将限制在有效范围内", angle);
      angle = ConstrainAngle(angle);
    }
   
    ServoCommand cmd = {CMD_SET_ANGLE, angle, 0, 0};
    xQueueSend(command_queue_, &cmd, portMAX_DELAY);
}

void ServoController::RotateClockwise(int degrees) {
    if (degrees <= 0) {
      ESP_LOGW(TAG4, "旋转角度必须大于0");
      return;
    }
   
    ServoCommand cmd = {CMD_ROTATE_CW, degrees, 0, 0};
    xQueueSend(command_queue_, &cmd, portMAX_DELAY);
}

void ServoController::RotateCounterclockwise(int degrees) {
    if (degrees <= 0) {
      ESP_LOGW(TAG4, "旋转角度必须大于0");
      return;
    }
   
    ServoCommand cmd = {CMD_ROTATE_CCW, degrees, 0, 0};
    xQueueSend(command_queue_, &cmd, portMAX_DELAY);
}

void ServoController::SweepBetween(int min_angle, int max_angle, int speed_ms) {
    if (!IsValidAngle(min_angle) || !IsValidAngle(max_angle)) {
      ESP_LOGW(TAG4, "扫描角度范围无效: %d - %d", min_angle, max_angle);
      return;
    }
   
    if (min_angle >= max_angle) {
      ESP_LOGW(TAG4, "最小角度必须小于最大角度");
      return;
    }
   
    ServoCommand cmd = {CMD_SWEEP, min_angle, max_angle, speed_ms};
    xQueueSend(command_queue_, &cmd, portMAX_DELAY);
}

void ServoController::Stop() {
    stop_requested_ = true;
    ServoCommand cmd = {CMD_STOP, 0, 0, 0};
    xQueueSend(command_queue_, &cmd, 0); // 不等待,立即发送停止命令
}

void ServoController::Reset() {
    ServoCommand cmd = {CMD_RESET, SERVO_DEFAULT_ANGLE, 0, 0};
    xQueueSend(command_queue_, &cmd, portMAX_DELAY);
}

void ServoController::WriteAngle(int angle) {
    angle = ConstrainAngle(angle);
    uint32_t compare_value = AngleToCompare(angle);
    ledc_set_duty(LEDC_LOW_SPEED_MODE, ledc_channel_, compare_value);
    ledc_update_duty(LEDC_LOW_SPEED_MODE, ledc_channel_);
    current_angle_ = angle;
}

uint32_t ServoController::AngleToCompare(int angle) {
    // 将角度转换为PWM占空比
    // SG90: 0.5ms-2.5ms 对应 0-180度
    // 50Hz周期 = 20ms
    // 占空比 = (脉宽 / 周期) * 2^14 (ESP32-S3使用14位分辨率)

    float pulse_width_ms = 0.5f + (angle / 180.0f) * 2.0f; // 0.5ms to 2.5ms
    float duty_cycle = pulse_width_ms / 20.0f; // 20ms period
    uint32_t compare_value = (uint32_t)(duty_cycle * 16383); // 14-bit resolution (2^14 - 1)

    return compare_value;
}

bool ServoController::IsValidAngle(int angle) const {
    return angle >= SERVO_MIN_DEGREE && angle <= SERVO_MAX_DEGREE;
}

int ServoController::ConstrainAngle(int angle) const {
    if (angle < SERVO_MIN_DEGREE) return SERVO_MIN_DEGREE;
    if (angle > SERVO_MAX_DEGREE) return SERVO_MAX_DEGREE;
    return angle;
}

void ServoController::ServoTask(void* parameter) {
    ServoController* controller = static_cast<ServoController*>(parameter);
    controller->ProcessCommands();
}

void ServoController::ProcessCommands() {
    ServoCommand cmd;
   
    while (true) {
      if (xQueueReceive(command_queue_, &cmd, pdMS_TO_TICKS(100)) == pdTRUE) {
            if (stop_requested_ && cmd.type != CMD_STOP) {
                continue; // 忽略非停止命令
            }
            
            switch (cmd.type) {
                case CMD_SET_ANGLE:
                  ExecuteSetAngle(cmd.param1);
                  break;
                  
                case CMD_ROTATE_CW:
                  ExecuteRotate(cmd.param1, true);
                  break;
                  
                case CMD_ROTATE_CCW:
                  ExecuteRotate(cmd.param1, false);
                  break;
                  
                case CMD_SWEEP:
                  ExecuteSweep(cmd.param1, cmd.param2, cmd.param3);
                  break;
                  
                case CMD_STOP:
                  is_moving_ = false;
                  is_sweeping_ = false;
                  stop_requested_ = false;
                  ESP_LOGI(TAG4, "舵机停止");
                  break;
                  
                case CMD_RESET:
                  ExecuteSetAngle(cmd.param1);
                  break;
            }
      }
    }
}

void ServoController::ExecuteSetAngle(int angle) {
    ESP_LOGI(TAG4, "设置舵机角度: %d度", angle);
    is_moving_ = true;
    SmoothMoveTo(angle, 500);
    is_moving_ = false;

    if (on_move_complete_callback_) {
      on_move_complete_callback_();
    }
}

void ServoController::ExecuteRotate(int degrees, bool clockwise) {
    int target = current_angle_ + (clockwise ? degrees : -degrees);
    target = ConstrainAngle(target);

    ESP_LOGI(TAG4, "%s旋转 %d度,从 %d度 到 %d度",
             clockwise ? "顺时针" : "逆时针", degrees, current_angle_, target);

    is_moving_ = true;
    SmoothMoveTo(target, 500);
    is_moving_ = false;

    if (on_move_complete_callback_) {
      on_move_complete_callback_();
    }
}

void ServoController::ExecuteSweep(int min_angle, int max_angle, int speed_ms) {
    ESP_LOGI(TAG4, "开始扫描模式: %d度 - %d度,速度: %dms", min_angle, max_angle, speed_ms);

    is_sweeping_ = true;
    is_moving_ = true;

    bool direction = true; // true = 向最大角度,false = 向最小角度

    while (is_sweeping_ && !stop_requested_) {
      int target = direction ? max_angle : min_angle;
      SmoothMoveTo(target, speed_ms);

      if (stop_requested_) break;

      direction = !direction;
      vTaskDelay(pdMS_TO_TICKS(100)); // 短暂停顿
    }

    is_sweeping_ = false;
    is_moving_ = false;

    ESP_LOGI(TAG4, "扫描模式结束");

    if (on_move_complete_callback_) {
      on_move_complete_callback_();
    }
}

void ServoController::SmoothMoveTo(int target_angle, int speed_ms) {
    target_angle = ConstrainAngle(target_angle);

    if (target_angle == current_angle_) {
      return; // 已经在目标位置
    }

    int start_angle = current_angle_;
    int angle_diff = target_angle - start_angle;
    int steps = abs(angle_diff);

    if (steps == 0) return;

    int delay_per_step = speed_ms / steps;
    if (delay_per_step < 10) delay_per_step = 10; // 最小延迟

    for (int i = 1; i <= steps && !stop_requested_; i++) {
      int current_step_angle = start_angle + (angle_diff * i) / steps;
      WriteAngle(current_step_angle);
      vTaskDelay(pdMS_TO_TICKS(delay_per_step));
    }

    // 确保到达精确位置
    if (!stop_requested_) {
      WriteAngle(target_angle);
    }
}‘df_k10_board.cc’文件代码:
#include "wifi_board.h"
#include "k10_audio_codec.h"
#include "display/lcd_display.h"
#include "esp_lcd_ili9341.h"
#include "led_control.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "esp32_camera.h"

#include "led/circular_strip.h"
#include "assets/lang_config.h"

#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <driver/i2c_master.h>
#include <driver/spi_common.h>
#include <wifi_station.h>

#include "esp_io_expander_tca95xx_16bit.h"
//#include "lamp_oc.h"
#include "servo_controller.h"


#define TAG "DF-K10"

LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(font_awesome_20_4);

class Df_K10Board : public WifiBoard {
private:
    i2c_master_bus_handle_t i2c_bus_;
    esp_io_expander_handle_t io_expander;
    LcdDisplay *display_;
    button_handle_t btn_a;
    button_handle_t btn_b;
    Esp32Camera* camera_;

    button_driver_t* btn_a_driver_ = nullptr;
    button_driver_t* btn_b_driver_ = nullptr;

    CircularStrip* led_strip_;
    CircularStrip* led_strip_my;
    static Df_K10Board* instance_;
    ServoController *horizontal_servo_controller_;
    ServoController *vertical_servo_controller_;
    void InitializeI2c() {
      // Initialize I2C peripheral
      i2c_master_bus_config_t i2c_bus_cfg = {
                .i2c_port = (i2c_port_t)1,
                .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
                .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
                .clk_source = I2C_CLK_SRC_DEFAULT,
                .glitch_ignore_cnt = 7,
                .intr_priority = 0,
                .trans_queue_depth = 0,
                .flags = {
                              .enable_internal_pullup = 1,
                        },
      };
      ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
    }

    void InitializeSpi() {
      spi_bus_config_t buscfg = {};
      buscfg.mosi_io_num = GPIO_NUM_21;
      buscfg.miso_io_num = GPIO_NUM_NC;
      buscfg.sclk_io_num = GPIO_NUM_12;
      buscfg.quadwp_io_num = GPIO_NUM_NC;
      buscfg.quadhd_io_num = GPIO_NUM_NC;
      buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
      ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
    }

    esp_err_t IoExpanderSetLevel(uint16_t pin_mask, uint8_t level) {
      return esp_io_expander_set_level(io_expander, pin_mask, level);
    }

    uint8_t IoExpanderGetLevel(uint16_t pin_mask) {
      uint32_t pin_val = 0;
      esp_io_expander_get_level(io_expander, DRV_IO_EXP_INPUT_MASK, &pin_val);
      pin_mask &= DRV_IO_EXP_INPUT_MASK;
      return (uint8_t)((pin_val & pin_mask) ? 1 : 0);
    }

    void InitializeIoExpander() {
      esp_io_expander_new_i2c_tca95xx_16bit(
                i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9555_ADDRESS_000, &io_expander);

      esp_err_t ret;
      ret = esp_io_expander_print_state(io_expander);
      if (ret != ESP_OK) {
            ESP_LOGE(TAG, "Print state failed: %s", esp_err_to_name(ret));
      }
      ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_0,
                                                                  IO_EXPANDER_OUTPUT);
      if (ret != ESP_OK) {
            ESP_LOGE(TAG, "Set direction failed: %s", esp_err_to_name(ret));
      }
      ret = esp_io_expander_set_level(io_expander, 0, 1);
      if (ret != ESP_OK) {
            ESP_LOGE(TAG, "Set level failed: %s", esp_err_to_name(ret));
      }
      ret = esp_io_expander_set_dir(
                io_expander, DRV_IO_EXP_INPUT_MASK,
                IO_EXPANDER_INPUT);
      if (ret != ESP_OK) {
            ESP_LOGE(TAG, "Set direction failed: %s", esp_err_to_name(ret));
      }
    }
    void InitializeButtons() {
      instance_ = this;

      // Button A
      button_config_t btn_a_config = {
            .long_press_time = 1000,
            .short_press_time = 0
      };
      btn_a_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t));
      btn_a_driver_->enable_power_save = false;
      btn_a_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t {
            return !instance_->IoExpanderGetLevel(IO_EXPANDER_PIN_NUM_2);
      };
      ESP_ERROR_CHECK(iot_button_create(&btn_a_config, btn_a_driver_, &btn_a));
      iot_button_register_cb(btn_a, BUTTON_SINGLE_CLICK, nullptr, [](void* button_handle, void* usr_data) {
            auto self = static_cast<Df_K10Board*>(usr_data);
            auto& app = Application::GetInstance();
            if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
                self->ResetWifiConfiguration();
            }
            app.ToggleChatState();
      }, this);
      iot_button_register_cb(btn_a, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) {
            auto self = static_cast<Df_K10Board*>(usr_data);
            auto codec = self->GetAudioCodec();
            auto volume = codec->output_volume() - 10;
            if (volume < 0) {
                volume = 0;
            }
            codec->SetOutputVolume(volume);
            self->GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
      }, this);

      // Button B
      button_config_t btn_b_config = {
            .long_press_time = 1000,
            .short_press_time = 0
      };
      btn_b_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t));
      btn_b_driver_->enable_power_save = false;
      btn_b_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t {
            return !instance_->IoExpanderGetLevel(IO_EXPANDER_PIN_NUM_12);
      };
      ESP_ERROR_CHECK(iot_button_create(&btn_b_config, btn_b_driver_, &btn_b));
      iot_button_register_cb(btn_b, BUTTON_SINGLE_CLICK, nullptr, [](void* button_handle, void* usr_data) {
            auto self = static_cast<Df_K10Board*>(usr_data);
            auto& app = Application::GetInstance();
            if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
                self->ResetWifiConfiguration();
            }
            app.ToggleChatState();
      }, this);
      iot_button_register_cb(btn_b, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) {
            auto self = static_cast<Df_K10Board*>(usr_data);
            auto codec = self->GetAudioCodec();
            auto volume = codec->output_volume() + 10;
            if (volume > 100) {
                volume = 100;
            }
            codec->SetOutputVolume(volume);
            self->GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
      }, this);
    }

    void InitializeCamera() {

      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_D2;
      config.pin_d1 = CAMERA_PIN_D3;
      config.pin_d2 = CAMERA_PIN_D4;
      config.pin_d3 = CAMERA_PIN_D5;
      config.pin_d4 = CAMERA_PIN_D6;
      config.pin_d5 = CAMERA_PIN_D7;
      config.pin_d6 = CAMERA_PIN_D8;
      config.pin_d7 = CAMERA_PIN_D9;
      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;               //这里如果写1 默认使用I2C1
      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);
    }

    void InitializeIli9341Display() {
      esp_lcd_panel_io_handle_t panel_io = nullptr;
      esp_lcd_panel_handle_t panel = nullptr;

      // 液晶屏控制IO初始化
      ESP_LOGD(TAG, "Install panel IO");
      esp_lcd_panel_io_spi_config_t io_config = {};
      io_config.cs_gpio_num = GPIO_NUM_14;
      io_config.dc_gpio_num = GPIO_NUM_13;
      io_config.spi_mode = 0;
      io_config.pclk_hz = 40 * 1000 * 1000;
      io_config.trans_queue_depth = 10;
      io_config.lcd_cmd_bits = 8;
      io_config.lcd_param_bits = 8;
      ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io));

      // 初始化液晶屏驱动芯片
      ESP_LOGD(TAG, "Install LCD driver");
      esp_lcd_panel_dev_config_t panel_config = {};
      panel_config.reset_gpio_num = GPIO_NUM_NC;
      panel_config.bits_per_pixel = 16;
      panel_config.color_space = ESP_LCD_COLOR_SPACE_BGR;

      ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel));
      ESP_ERROR_CHECK(esp_lcd_panel_reset(panel));
      ESP_ERROR_CHECK(esp_lcd_panel_init(panel));
      ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel, DISPLAY_BACKLIGHT_OUTPUT_INVERT));
      ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY));
      ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y));
      ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true));

      display_ = new SpiLcdDisplay(panel_io, panel,
                              DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY,
                              {
                                        .text_font = &font_puhui_20_4,
                                        .icon_font = &font_awesome_20_4,
                                        .emoji_font = font_emoji_64_init(),
                              });
    }
    void InitializeServoController()
    {
      ESP_LOGI(TAG, "初始化SG90舵机控制器");
      // 初始化水平舵机控制器
    horizontal_servo_controller_ = new ServoController(SERVO_PIN_HORIZONTAL);
    if (!horizontal_servo_controller_->Initialize()) {
      ESP_LOGE(TAG, "水平舵机初始化失败");
      delete horizontal_servo_controller_;
      horizontal_servo_controller_ = nullptr;
    }

    // 初始化垂直舵机控制器
    vertical_servo_controller_ = new ServoController(SERVO_PIN_VERTICAL);
    if (!vertical_servo_controller_->Initialize()) {
      ESP_LOGE(TAG, "垂直舵机初始化失败");
      delete vertical_servo_controller_;
      vertical_servo_controller_ = nullptr;
    }

      ESP_LOGI(TAG, "SG90舵机控制器初始化完成");
    }

    // 物联网初始化,添加对 AI 可见设备
    void InitializeIot() {

      led_strip_ = new CircularStrip(GPIO_NUM_46, 3);
      new LedStripControl(led_strip_);
      //static lamp_light lamp_light(GPIO_NUM_1);
   
    auto& mcp_server = McpServer::GetInstance();

    // 水平舵机工具
    mcp_server.AddTool("self.servo.horizontal.set_angle",
                     "设置水平舵机角度。angle: 目标角度(0-180度)",
                     PropertyList({Property("angle", kPropertyTypeInteger, 90, 0, 180)}),
                     (const PropertyList& properties) -> ReturnValue {
                           int angle = properties["angle"].value<int>();
                           SetHorizontalAngle(angle);
                           return "水平舵机设置到 " + std::to_string(angle) + " 度";
                     });

    mcp_server.AddTool("self.servo.horizontal.rotate_clockwise",
                     "水平舵机顺时针旋转。degrees: 旋转角度(1-180度)",
                     PropertyList({Property("degrees", kPropertyTypeInteger, 30, 1, 180)}),
                     (const PropertyList& properties) -> ReturnValue {
                           int degrees = properties["degrees"].value<int>();
                           RotateHorizontalClockwise(degrees);
                           return "水平舵机顺时针旋转 " + std::to_string(degrees) + " 度";
                     });

    mcp_server.AddTool("self.servo.horizontal.rotate_counterclockwise",
                     "水平舵机逆时针旋转。degrees: 旋转角度(1-180度)",
                     PropertyList({Property("degrees", kPropertyTypeInteger, 30, 1, 180)}),
                     (const PropertyList& properties) -> ReturnValue {
                           int degrees = properties["degrees"].value<int>();
                           RotateHorizontalCounterclockwise(degrees);
                           return "水平舵机逆时针旋转 " + std::to_string(degrees) + " 度";
                     });

    // 垂直舵机工具
    mcp_server.AddTool("self.servo.vertical.set_angle",
                     "设置垂直舵机角度。angle: 目标角度(0-180度)",
                     PropertyList({Property("angle", kPropertyTypeInteger, 90, 0, 180)}),
                     (const PropertyList& properties) -> ReturnValue {
                           int angle = properties["angle"].value<int>();
                           SetVerticalAngle(angle);
                           return "垂直舵机设置到 " + std::to_string(angle) + " 度";
                     });

    mcp_server.AddTool("self.servo.vertical.rotate_clockwise",
                     "垂直舵机顺时针旋转。degrees: 旋转角度(1-180度)",
                     PropertyList({Property("degrees", kPropertyTypeInteger, 30, 1, 180)}),
                     (const PropertyList& properties) -> ReturnValue {
                           int degrees = properties["degrees"].value<int>();
                           RotateVerticalClockwise(degrees);
                           return "垂直舵机顺时针旋转 " + std::to_string(degrees) + " 度";
                     });

    mcp_server.AddTool("self.servo.vertical.rotate_counterclockwise",
                     "垂直舵机逆时针旋转。degrees: 旋转角度(1-180度)",
                     PropertyList({Property("degrees", kPropertyTypeInteger, 30, 1, 180)}),
                     (const PropertyList& properties) -> ReturnValue {
                           int degrees = properties["degrees"].value<int>();
                           RotateVerticalCounterclockwise(degrees);
                           return "垂直舵机逆时针旋转 " + std::to_string(degrees) + " 度";
                     });
      
    mcp_server.AddTool("self.servo.Vertical.HigH",
                        "抬头",
                        PropertyList(),
                        (const PropertyList& properties) -> ReturnValue {
                            HorizontalReset();
                            SetVerticalAngle(90);
                            return "已抬头";
                        });
    mcp_server.AddTool("self.servo.Vertical.LOW",
                            "低头",
                            PropertyList(),
                            (const PropertyList& properties) -> ReturnValue {
                              HorizontalReset();
                              SetVerticalAngle(160);
                              return "已低头";
                            });
    /*
    mcp_server.AddTool("self.servo.vertical.stop",
      "立即停止垂直舵机运动",
      PropertyList(),
      (const PropertyList& properties) -> ReturnValue {
            VerticalStop();
            return "垂直舵机已停止";
            
      });
      mcp_server.AddTool("self.servo.horizontal.stop",
            "立即停止水平舵机运动",
            PropertyList(),
            (const PropertyList& properties) -> ReturnValue {
                HorizontalStop();
                return "水平舵机已停止";
               
      });
      
      // 复位到中心位置
      mcp_server.AddTool("self.servo.Vertical.reset",
            "将垂直舵机复位到中心位置(90度)",
            PropertyList(),
            (const PropertyList& properties) -> ReturnValue {
                VerticalReset();
                return "垂直舵机已复位到中心位置(90度)";
            });
      mcp_server.AddTool("self.servo.Horizontal.reset",
            "将水平舵机复位到中心位置(90度)",
            PropertyList(),
            (const PropertyList& properties) -> ReturnValue {
                HorizontalReset();
                return "水平舵机已复位到中心位置(90度)";
      });
         // 获取当前位置
      mcp_server.AddTool("self.servo.Vertical.get_position",
            "获取垂直舵机当前角度位置",
            PropertyList(),
            (const PropertyList& properties) -> ReturnValue {
                int angle = VerticalGetCurrentAngle();
             return "当前垂直舵机角度: " + std::to_string(angle) + " 度";
      });
      mcp_server.AddTool("self.servo.Horizontal.get_position",
            "获取水平舵机当前角度位置",
            PropertyList(),
            (const PropertyList& properties) -> ReturnValue {
                int angle = HorizontalGetCurrentAngle();
             return "当前水平舵机角度: " + std::to_string(angle) + " 度";
      });
      
      // 扫描模式
      mcp_server.AddTool("self.servo.Vertical.sweep",
             "垂直舵机摆动模式,在指定角度范围内来回摆动。"
            "min_angle: 最小角度(91-179度); max_angle: 最大角度(92-180度); "
            "speed: 摆动速度,毫秒(100-5000ms)",
            PropertyList({Property("min_angle", kPropertyTypeInteger, 0, 0, 179),
                      Property("max_angle", kPropertyTypeInteger, 180, 1, 180),
                      Property("speed", kPropertyTypeInteger, 1000, 100, 5000)}),
            (const PropertyList& properties) -> ReturnValue {
                int min_angle = properties["min_angle"].value<int>();
                int max_angle = properties["max_angle"].value<int>();
                int speed = properties["speed"].value<int>();
                VerticalSweepBetween(min_angle, max_angle, speed);
                return "垂直方向开始扫描模式: " + std::to_string(min_angle) + "° - " +
                   std::to_string(max_angle) + "°";
      });
      mcp_server.AddTool("self.servo.Horizontal.sweep",
            "水平舵机摆动模式,在指定角度范围内来回摆动。"
         "min_angle: 最小角度(0-179度); max_angle: 最大角度(1-180度); "
         "speed: 摆动速度,毫秒(100-5000ms)",
         PropertyList({Property("min_angle", kPropertyTypeInteger, 0, 0, 179),
                     Property("max_angle", kPropertyTypeInteger, 180, 1, 180),
                     Property("speed", kPropertyTypeInteger, 1000, 100, 5000)}),
         (const PropertyList& properties) -> ReturnValue {
               int min_angle = properties["min_angle"].value<int>();
               int max_angle = properties["max_angle"].value<int>();
               int speed = properties["speed"].value<int>();
               HorizontalSweepBetween(min_angle, max_angle, speed);
               return "水平方向开始扫描模式: " + std::to_string(min_angle) + "° - " +
                  std::to_string(max_angle) + "°";
       });

       */
}

public:
    Df_K10Board() {
      InitializeI2c();
      InitializeIoExpander();
      InitializeSpi();
      InitializeIli9341Display();
      InitializeButtons();
      InitializeCamera();
      InitializeServoController();
      InitializeIot();

      

    }
   
    virtual ~Df_K10Board()
    {
      if (horizontal_servo_controller_) {
            delete horizontal_servo_controller_;
      }
      if (vertical_servo_controller_) {
            delete vertical_servo_controller_;
      }
         
    }
   
   virtual Led* GetLed() override {
      return led_strip_;
    }
   
    void SetHorizontalAngle(int angle) {
      if (horizontal_servo_controller_) {
            horizontal_servo_controller_->SetAngle(angle);
      }
    }
   
    void SetVerticalAngle(int angle) {
      if (vertical_servo_controller_) {
            vertical_servo_controller_->SetAngle(angle);
      }
    }
   
    void RotateHorizontalClockwise(int degrees) {
      if (horizontal_servo_controller_) {
            horizontal_servo_controller_->RotateClockwise(degrees);
      }
    }
   
    void RotateHorizontalCounterclockwise(int degrees) {
      if (horizontal_servo_controller_) {
            horizontal_servo_controller_->RotateCounterclockwise(degrees);
      }
    }
   
    void RotateVerticalClockwise(int degrees) {
      if (vertical_servo_controller_) {
            vertical_servo_controller_->RotateClockwise(degrees);
      }
    }
   
    void RotateVerticalCounterclockwise(int degrees) {
      if (vertical_servo_controller_) {
            vertical_servo_controller_->RotateCounterclockwise(degrees);
      }
    }
    void VerticalStop() {
      if (vertical_servo_controller_) {
            vertical_servo_controller_->Stop();
      }
    }
    void HorizontalStop() {
      if (horizontal_servo_controller_) {
            horizontal_servo_controller_->Stop();
      }
    }
    void VerticalReset() {
      if (vertical_servo_controller_) {
            vertical_servo_controller_->Reset();
      }
    }
    void HorizontalReset() {
      if (horizontal_servo_controller_) {
            horizontal_servo_controller_->Reset();
      }
    }
    int VerticalGetCurrentAngle() {
      if (vertical_servo_controller_) {
         return vertical_servo_controller_->GetCurrentAngle();
      }
      return -1;
    }
    int HorizontalGetCurrentAngle() {
      if (horizontal_servo_controller_) {
         return horizontal_servo_controller_->GetCurrentAngle();
      }
      return -1;
    }
    void VerticalSweepBetween(int min_angle, int max_angle, int speed_ms = 1000) {
      if (vertical_servo_controller_) {
            vertical_servo_controller_->SweepBetween(min_angle, max_angle,1000);
      }
    }
    void HorizontalSweepBetween(int min_angle, int max_angle, int speed_ms = 1000) {
      if (horizontal_servo_controller_) {
            horizontal_servo_controller_->SweepBetween(min_angle, max_angle,1000);
      }
    }
   
    virtual AudioCodec *GetAudioCodec() override {
      static K10AudioCodec audio_codec(
                  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,
                  AUDIO_CODEC_PA_PIN,
                  AUDIO_CODEC_ES8311_ADDR,
                  AUDIO_CODEC_ES7210_ADDR,
                  AUDIO_INPUT_REFERENCE);
      return &audio_codec;
    }

    virtual Camera* GetCamera() override {
      return camera_;
    }

    virtual Display *GetDisplay() override {
      return display_;
    }
};

DECLARE_BOARD(Df_K10Board);

Df_K10Board* Df_K10Board::instance_ = nullptr;

【拍照与图像识别功能】

行空板 K10 自带的摄像头接口使得拍照功能的实现变得非常简单。我们通过 ESP-IDF 提供的摄像头驱动库,初始化摄像头,并配置好相关的参数,如分辨率、像素格式等。
图像识别功能,是利用小智AI的图像识别功,当用户有图片识别需求时,服务端识别到这种意图,会通过 MCP 向客户端发请求。 客户端,调用摄像头拍照,然后请求视觉识别模型,获得当前图片的描述。 客户端通过 MCP 把当前看到的发给服务端,由服务端 LLM 进行总结后回答用户。在一些应用场景中,可以通过关键词来触发图像识别功能。例如,当你说出“识别画面”等关键词时,系统会自动启动图像识别。

【视频演示】
https://www.bilibili.com/video/BV1xfa2z3EKL/?share_source=copy_web&vd_source=98855d5b99ff76982639c5ca6ff6f528
【总结】

通过这个项目,我们不仅实现了一个有趣且实用的智能舵机云台与拍照识别系统,还深入学习了行空板 K10 的开发、舵机控制以及图像识别等知识。这个项目展示了开源硬件和软件的强大潜力,激发了我们探索更多创新应用的热情。希望这篇文章能够为有志于从事智能硬件开发的创客朋友们提供一些思路和启发,让我们一起动手创造更多有趣的作品吧!

页: [1]
查看完整版本: 行空板K10打造智能舵机云台与拍照识别系统