From e39a46c1a0f5563f451ffa72839322af0ec34405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=B9=8F?= <52451470+txp666@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:04:06 +0800 Subject: [PATCH] =?UTF-8?q?otto=E6=96=B0=E5=A2=9E=E5=8A=A8=E4=BD=9C?= =?UTF-8?q?=E5=92=8CAI=E8=87=AA=E5=AE=9A=E4=B9=89=E7=BC=96=E7=A8=8B?= =?UTF-8?q?=E5=8A=A8=E4=BD=9CMCP=E5=B7=A5=E5=85=B7=20(#1365)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * otto v1.4.0 MCP 1.使用MCP协议控制机器人 2.gif继承lcdDisplay,避免修改lcdDisplay * otto v1.4.1 gif as components gif as components * electronBot v1.1.0 mcp 1.增加electronBot支持 2.mcp协议 3.gif 作为组件 4.display子类 * 规范代码 1.规范代码 2.修复切换主题死机bug * fix(ota): 修复 ottoRobot和electronBot OTA 升级崩溃问题 bug * 1.增加robot舵机初始位置校准 2.fix(mcp_sever) 超出范围异常捕获类型 bug * refactor: Update Electron and Otto emoji display implementations - Removed GIF selection from Kconfig for Electron and Otto boards. - Updated Electron and Otto bot versions to 2.0.4 in their respective config files. - Refactored emoji display classes to utilize EmojiCollection for managing emojis. - Enhanced chat label setup and status display functionality in both classes. - Cleaned up unused code and improved initialization logging for emoji displays. * Rename OTTO_ICON_FONT.c to otto_icon_font.c * Rename OTTO_ICON_FONT.c to otto_icon_font.c * refactor: Update Otto emoji display configurations and functionalities - Changed chat label text mode to circular scrolling for both Otto and Electron emoji displays. - Bumped Otto robot version to 2.0.5 in the configuration file. - Added new actions for Otto robot including Sit, WhirlwindLeg, Fitness, Greeting, Shy, RadioCalisthenics, MagicCircle, and Showcase. - Enhanced servo sequence handling and added support for executing custom servo sequences. - Improved logging and error handling for servo sequence execution. * refactor: Update chat label long mode for Electron and Otto emoji displays - Changed chat label text mode from wrap to circular scrolling for both Electron and Otto emoji displays. - Improved consistency in chat label setup across both implementations. * Update Otto robot README with new actions and parameters --- .../electron-bot/electron_emoji_display.cc | 2 +- main/boards/otto-robot/README.md | 61 +- main/boards/otto-robot/config.h | 2 +- main/boards/otto-robot/oscillator.cc | 8 + main/boards/otto-robot/oscillator.h | 8 + main/boards/otto-robot/otto_controller.cc | 638 +++++++++++++++--- main/boards/otto-robot/otto_emoji_display.cc | 2 +- main/boards/otto-robot/otto_movements.cc | 385 ++++++++--- main/boards/otto-robot/otto_movements.h | 16 +- 9 files changed, 939 insertions(+), 183 deletions(-) diff --git a/main/boards/electron-bot/electron_emoji_display.cc b/main/boards/electron-bot/electron_emoji_display.cc index 0a706464..2bb82481 100644 --- a/main/boards/electron-bot/electron_emoji_display.cc +++ b/main/boards/electron-bot/electron_emoji_display.cc @@ -89,7 +89,7 @@ void ElectronEmojiDisplay::SetupChatLabel() { chat_message_label_ = lv_label_create(container_); lv_label_set_text(chat_message_label_, ""); lv_obj_set_width(chat_message_label_, width_ * 0.9); // 限制宽度为屏幕宽度的 90% - lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_WRAP); // 设置为自动换行模式 + lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); // 设置为自动换行模式 lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); // 设置文本居中对齐 lv_obj_set_style_text_color(chat_message_label_, lv_color_white(), 0); SetTheme(LvglThemeManager::GetInstance().GetTheme("dark")); diff --git a/main/boards/otto-robot/README.md b/main/boards/otto-robot/README.md index e7f48194..bc311f3a 100644 --- a/main/boards/otto-robot/README.md +++ b/main/boards/otto-robot/README.md @@ -21,8 +21,8 @@ otto 机器人是一个开源的人形机器人平台,具有多种动作能力 > > **我的动作能力**: > - **基础移动**: 行走(前后), 转向(左右), 跳跃 -> - **特殊动作**: 摇摆, 太空步, 弯曲身体, 摇腿, 上下运动 -> - **手部动作**: 举手, 放手, 挥手 (仅在配置手部舵机时可用) +> - **特殊动作**: 摇摆, 太空步, 弯曲身体, 摇腿, 上下运动, 旋风腿, 坐下, 展示动作 +> - **手部动作**: 举手, 放手, 挥手, 大风车, 起飞, 健身, 打招呼, 害羞, 广播体操, 爱的魔力转圈圈 (仅在配置手部舵机时可用) > > **我的个性特点**: > - 我有强迫症,每次说话都要根据我的心情随机做一个动作(先发送动作指令再说话) @@ -58,37 +58,57 @@ otto 机器人具有丰富的动作能力,包括行走、转向、跳跃、摇 | self.otto.moonwalk | 太空步 | **steps**: 太空步步数(1-100,默认3)
**speed**: 速度(500-1500,数值越小越快,默认1000)
**direction**: 方向(1=左, -1=右,默认1)
**amount**: 幅度(0-170度,默认25) | | self.otto.bend | 弯曲身体 | **steps**: 弯曲次数(1-100,默认1)
**speed**: 弯曲速度(500-1500,数值越小越快,默认1000)
**direction**: 弯曲方向(1=左, -1=右,默认1) | | self.otto.shake_leg | 摇腿 | **steps**: 摇腿次数(1-100,默认1)
**speed**: 摇腿速度(500-1500,数值越小越快,默认1000)
**direction**: 腿部选择(1=左腿, -1=右腿,默认1) | +| self.otto.sit | 坐下 | 不需要参数 | +| self.otto.showcase | 展示动作 | 不需要参数。串联执行多个动作:往前走3步、挥挥手、跳舞(广播体操)、太空步、摇摆、起飞、健身、往后走3步 | | self.otto.updown | 上下运动 | **steps**: 上下运动次数(1-100,默认3)
**speed**: 运动速度(500-1500,数值越小越快,默认1000)
**amount**: 运动幅度(0-170度,默认20) | +| self.otto.whirlwind_leg | 旋风腿 | **steps**: 动作次数(3-100,默认3)
**speed**: 动作速度(100-1000,数值越小越快,建议300)
**amplitude**: 踢腿幅度(20-40度,默认30) | | self.otto.hands_up | 举手 * | **speed**: 举手速度(500-1500,数值越小越快,默认1000)
**direction**: 手部选择(1=左手, -1=右手, 0=双手,默认1) | | self.otto.hands_down | 放手 * | **speed**: 放手速度(500-1500,数值越小越快,默认1000)
**direction**: 手部选择(1=左手, -1=右手, 0=双手,默认1) | -| self.otto.hand_wave | 挥手 * | **speed**: 挥手速度(500-1500,数值越小越快,默认1000)
**direction**: 手部选择(1=左手, -1=右手, 0=双手,默认1) | +| self.otto.hand_wave | 挥手 * | **direction**: 手部选择(1=左手, -1=右手, 0=双手,默认1) | +| self.otto.windmill | 大风车 * | **steps**: 动作次数(3-100,默认6)
**speed**: 动作周期(300-2000毫秒,数值越小越快,默认500)
**amplitude**: 振荡幅度(50-90度,默认70) | +| self.otto.takeoff | 起飞 * | **steps**: 动作次数(5-100,默认5)
**speed**: 动作周期(200-600毫秒,数值越小越快,建议300)
**amplitude**: 振荡幅度(20-60度,默认40) | +| self.otto.fitness | 健身 * | **steps**: 动作次数(3-100,默认5)
**speed**: 动作速度(500-2000毫秒,数值越小越快,默认1000)
**amplitude**: 振荡幅度(10-50度,默认25) | +| self.otto.greeting | 打招呼 * | **direction**: 手部选择(1=左手, -1=右手,默认1)
**steps**: 动作次数(3-100,默认5) | +| self.otto.shy | 害羞 * | **direction**: 方向(1=左, -1=右,默认1)
**steps**: 动作次数(3-100,默认5) | +| self.otto.radio_calisthenics | 广播体操 * | 不需要参数 | +| self.otto.magic_circle | 爱的魔力转圈圈 * | 不需要参数 | **注**: 标记 * 的手部动作仅在配置了手部舵机时可用。 ### 系统工具 -| MCP工具名称 | 描述 | 返回值 | +| MCP工具名称 | 描述 | 返回值/说明 | |-------------------|-----------------|---------------------------------------------------| -| self.otto.stop | 立即停止 | 停止当前动作并回到初始位置 | +| self.otto.home | 复位机器人到初始位置 | 不需要参数 | +| self.otto.stop | 立即停止所有动作并复位 | 停止当前动作并回到初始位置 | | self.otto.get_status | 获取机器人状态 | 返回 "moving" 或 "idle" | +| self.otto.set_trim | 校准单个舵机位置 | **servo_type**: 舵机类型(left_leg/right_leg/left_foot/right_foot/left_hand/right_hand)
**trim_value**: 微调值(-50到50度) | +| self.otto.get_trims | 获取当前的舵机微调设置 | 返回所有舵机微调值的JSON格式 | | self.battery.get_level | 获取电池状态 | 返回电量百分比和充电状态的JSON格式 | +| self.otto.servo_sequences | 舵机序列自编程 | 支持分段发送序列,支持普通移动和振荡器两种模式。详见代码注释中的详细说明 | ### 参数说明 1. **steps**: 动作执行的步数/次数,数值越大动作持续时间越长 -2. **speed**: 动作执行速度,数值范围500-1500,**数值越小越快** +2. **speed**: 动作执行速度/周期,**数值越小越快** + - 大多数动作: 500-1500毫秒 + - 特殊动作可能有所不同(如旋风腿: 100-1000,起飞: 200-600等) + - 具体范围请参考各动作的说明 3. **direction**: 方向参数 - 移动动作: 1=左/前进, -1=右/后退 - 手部动作: 1=左手, -1=右手, 0=双手 -4. **amount/arm_swing**: 动作幅度,范围0-170度 +4. **amount/amplitude/arm_swing**: 动作幅度,范围根据动作而定(通常0-170度) - 0表示不摆动(适用于手臂摆动) - 数值越大幅度越大 + - 不同动作可能有不同的幅度范围限制 ### 动作控制 - 每个动作执行完成后,机器人会自动回到初始位置(home),以便于执行下一个动作 +- **例外**: `sit`(坐下)和 `showcase`(展示动作)执行后不会自动复位 - 所有参数都有合理的默认值,可以省略不需要自定义的参数 - 动作在后台任务中执行,不会阻塞主程序 - 支持动作队列,可以连续执行多个动作 +- 手部动作需要配置手部舵机才能使用,如果没有配置手部舵机,相关动作将被跳过 ### MCP工具调用示例 ```json @@ -107,6 +127,18 @@ otto 机器人具有丰富的动作能力,包括行走、转向、跳跃、摇 // 挥左手打招呼 {"name": "self.otto.hand_wave", "arguments": {"direction": 1}} +// 展示动作(串联多个动作) +{"name": "self.otto.showcase", "arguments": {}} + +// 大风车动作 +{"name": "self.otto.windmill", "arguments": {"steps": 10, "amplitude": 80}} + +// 起飞动作 +{"name": "self.otto.takeoff", "arguments": {"steps": 5, "speed": 300}} + +// 广播体操 +{"name": "self.otto.radio_calisthenics", "arguments": {}} + // 立即停止 {"name": "self.otto.stop", "arguments": {}} ``` @@ -115,9 +147,20 @@ otto 机器人具有丰富的动作能力,包括行走、转向、跳跃、摇 - "向前走" / "向前走5步" / "快速向前" - "左转" / "右转" / "转身" - "跳跃" / "跳一下" -- "摇摆" / "跳舞" +- "摇摆" / "摇摆舞" / "跳舞" - "太空步" / "月球漫步" -- "挥手" / "举手" / "放手" +- "旋风腿" / "旋风腿动作" +- "坐下" / "坐下休息" +- "展示动作" / "表演一下" +- "挥手" / "挥手打招呼" +- "举手" / "双手举起" / "放手" +- "大风车" / "做大风车" +- "起飞" / "准备起飞" +- "健身" / "做健身动作" +- "打招呼" / "打招呼动作" +- "害羞" / "害羞动作" +- "广播体操" / "做广播体操" +- "爱的魔力转圈圈" / "转圈圈" - "停止" / "停下" **说明**: 小智控制机器人动作是创建新的任务在后台控制,动作执行期间仍可接受新的语音指令。可以通过"停止"语音指令立即停下Otto。 diff --git a/main/boards/otto-robot/config.h b/main/boards/otto-robot/config.h index ab5c9a65..76a8a6ac 100644 --- a/main/boards/otto-robot/config.h +++ b/main/boards/otto-robot/config.h @@ -47,6 +47,6 @@ #define BOOT_BUTTON_GPIO GPIO_NUM_0 -#define OTTO_ROBOT_VERSION "2.0.4" +#define OTTO_ROBOT_VERSION "2.0.5" #endif // _BOARD_CONFIG_H_ diff --git a/main/boards/otto-robot/oscillator.cc b/main/boards/otto-robot/oscillator.cc index adca7ac1..8dad82c0 100644 --- a/main/boards/otto-robot/oscillator.cc +++ b/main/boards/otto-robot/oscillator.cc @@ -1,3 +1,11 @@ +//-------------------------------------------------------------- +//-- Oscillator.pde +//-- Generate sinusoidal oscillations in the servos +//-------------------------------------------------------------- +//-- (c) Juan Gonzalez-Gomez (Obijuan), Dec 2011 +//-- (c) txp666 for esp32, 202503 +//-- GPL license +//-------------------------------------------------------------- #include "oscillator.h" #include diff --git a/main/boards/otto-robot/oscillator.h b/main/boards/otto-robot/oscillator.h index d9e79f25..c37a69a8 100644 --- a/main/boards/otto-robot/oscillator.h +++ b/main/boards/otto-robot/oscillator.h @@ -1,3 +1,11 @@ +//-------------------------------------------------------------- +//-- Oscillator.pde +//-- Generate sinusoidal oscillations in the servos +//-------------------------------------------------------------- +//-- (c) Juan Gonzalez-Gomez (Obijuan), Dec 2011 +//-- (c) txp666 for esp32, 202503 +//-- GPL license +//-------------------------------------------------------------- #ifndef __OSCILLATOR_H__ #define __OSCILLATOR_H__ diff --git a/main/boards/otto-robot/otto_controller.cc b/main/boards/otto-robot/otto_controller.cc index 56972337..10d452bf 100644 --- a/main/boards/otto-robot/otto_controller.cc +++ b/main/boards/otto-robot/otto_controller.cc @@ -5,6 +5,7 @@ #include #include +#include #include #include "application.h" @@ -31,6 +32,7 @@ private: int speed; int direction; int amount; + char servo_sequence_json[512]; // 用于存储舵机序列的JSON字符串 }; enum ActionType { @@ -41,6 +43,9 @@ private: ACTION_MOONWALK = 5, ACTION_BEND = 6, ACTION_SHAKE_LEG = 7, + ACTION_SIT = 25, // 坐下 + ACTION_RADIO_CALISTHENICS = 26, // 广播体操 + ACTION_MAGIC_CIRCLE = 27, // 爱的魔力转圈圈 ACTION_UPDOWN = 8, ACTION_TIPTOE_SWING = 9, ACTION_JITTER = 10, @@ -50,7 +55,15 @@ private: ACTION_HANDS_UP = 14, ACTION_HANDS_DOWN = 15, ACTION_HAND_WAVE = 16, - ACTION_HOME = 17 + ACTION_WINDMILL = 20, // 大风车 + ACTION_TAKEOFF = 21, // 起飞 + ACTION_FITNESS = 22, // 健身 + ACTION_GREETING = 23, // 打招呼 + ACTION_SHY = 24, // 害羞 + ACTION_SHOWCASE = 28, // 展示动作 + ACTION_HOME = 17, + ACTION_SERVO_SEQUENCE = 18, // 舵机序列(自编程) + ACTION_WHIRLWIND_LEG = 19 // 旋风腿 }; static void ActionTask(void* arg) { @@ -62,73 +75,355 @@ private: if (xQueueReceive(controller->action_queue_, ¶ms, pdMS_TO_TICKS(1000)) == pdTRUE) { ESP_LOGI(TAG, "执行动作: %d", params.action_type); controller->is_action_in_progress_ = true; - - switch (params.action_type) { - case ACTION_WALK: - controller->otto_.Walk(params.steps, params.speed, params.direction, - params.amount); - break; - case ACTION_TURN: - controller->otto_.Turn(params.steps, params.speed, params.direction, - params.amount); - break; - case ACTION_JUMP: - controller->otto_.Jump(params.steps, params.speed); - break; - case ACTION_SWING: - controller->otto_.Swing(params.steps, params.speed, params.amount); - break; - case ACTION_MOONWALK: - controller->otto_.Moonwalker(params.steps, params.speed, params.amount, - params.direction); - break; - case ACTION_BEND: - controller->otto_.Bend(params.steps, params.speed, params.direction); - break; - case ACTION_SHAKE_LEG: - controller->otto_.ShakeLeg(params.steps, params.speed, params.direction); - break; - case ACTION_UPDOWN: - controller->otto_.UpDown(params.steps, params.speed, params.amount); - break; - case ACTION_TIPTOE_SWING: - controller->otto_.TiptoeSwing(params.steps, params.speed, params.amount); - break; - case ACTION_JITTER: - controller->otto_.Jitter(params.steps, params.speed, params.amount); - break; - case ACTION_ASCENDING_TURN: - controller->otto_.AscendingTurn(params.steps, params.speed, params.amount); - break; - case ACTION_CRUSAITO: - controller->otto_.Crusaito(params.steps, params.speed, params.amount, - params.direction); - break; - case ACTION_FLAPPING: - controller->otto_.Flapping(params.steps, params.speed, params.amount, - params.direction); - break; - case ACTION_HANDS_UP: - if (controller->has_hands_) { - controller->otto_.HandsUp(params.speed, params.direction); + if (params.action_type == ACTION_SERVO_SEQUENCE) { + // 执行舵机序列(自编程)- 仅支持短键名格式 + cJSON* json = cJSON_Parse(params.servo_sequence_json); + if (json != nullptr) { + ESP_LOGD(TAG, "JSON解析成功,长度=%d", strlen(params.servo_sequence_json)); + // 使用短键名 "a" 表示动作数组 + cJSON* actions = cJSON_GetObjectItem(json, "a"); + if (cJSON_IsArray(actions)) { + int array_size = cJSON_GetArraySize(actions); + ESP_LOGI(TAG, "执行舵机序列,共%d个动作", array_size); + + // 获取序列执行完成后的延迟(短键名 "d",顶层参数) + int sequence_delay = 0; + cJSON* delay_item = cJSON_GetObjectItem(json, "d"); + if (cJSON_IsNumber(delay_item)) { + sequence_delay = delay_item->valueint; + if (sequence_delay < 0) sequence_delay = 0; + } + + // 初始化当前舵机位置(用于保持未指定的舵机位置) + int current_positions[SERVO_COUNT]; + for (int j = 0; j < SERVO_COUNT; j++) { + current_positions[j] = 90; // 默认中间位置 + } + // 手部舵机默认位置 + current_positions[LEFT_HAND] = 45; + current_positions[RIGHT_HAND] = 180 - 45; + + for (int i = 0; i < array_size; i++) { + cJSON* action_item = cJSON_GetArrayItem(actions, i); + if (cJSON_IsObject(action_item)) { + // 检查是否为振荡器模式(短键名 "osc") + cJSON* osc_item = cJSON_GetObjectItem(action_item, "osc"); + if (cJSON_IsObject(osc_item)) { + // 振荡器模式 - 使用Execute2,以绝对角度为中心振荡 + int amplitude[SERVO_COUNT] = {0}; + int center_angle[SERVO_COUNT] = {0}; + double phase_diff[SERVO_COUNT] = {0}; + int period = 500; // 默认周期500毫秒 + float steps = 5.0; // 默认步数5.0 + + const char* servo_names[] = {"ll", "rl", "lf", "rf", "lh", "rh"}; + + // 读取振幅(短键名 "a"),默认20度 + for (int j = 0; j < SERVO_COUNT; j++) { + amplitude[j] = 20; // 默认振幅20度 + } + cJSON* amp_item = cJSON_GetObjectItem(osc_item, "a"); + if (cJSON_IsObject(amp_item)) { + for (int j = 0; j < SERVO_COUNT; j++) { + cJSON* amp_value = cJSON_GetObjectItem(amp_item, servo_names[j]); + if (cJSON_IsNumber(amp_value)) { + int amp = amp_value->valueint; + if (amp >= 10 && amp <= 90) { + amplitude[j] = amp; + } + } + } + } + + // 读取中心角度(短键名 "o"),默认90度(绝对角度0-180度) + for (int j = 0; j < SERVO_COUNT; j++) { + center_angle[j] = 90; // 默认中心角度90度(中间位置) + } + cJSON* center_item = cJSON_GetObjectItem(osc_item, "o"); + if (cJSON_IsObject(center_item)) { + for (int j = 0; j < SERVO_COUNT; j++) { + cJSON* center_value = cJSON_GetObjectItem(center_item, servo_names[j]); + if (cJSON_IsNumber(center_value)) { + int center = center_value->valueint; + if (center >= 0 && center <= 180) { + center_angle[j] = center; + } + } + } + } + + // 安全检查:防止左右腿脚同时做大幅度振荡(振幅检查) + const int LARGE_AMPLITUDE_THRESHOLD = 40; // 大幅度振幅阈值:40度 + bool left_leg_large = amplitude[LEFT_LEG] >= LARGE_AMPLITUDE_THRESHOLD; + bool right_leg_large = amplitude[RIGHT_LEG] >= LARGE_AMPLITUDE_THRESHOLD; + bool left_foot_large = amplitude[LEFT_FOOT] >= LARGE_AMPLITUDE_THRESHOLD; + bool right_foot_large = amplitude[RIGHT_FOOT] >= LARGE_AMPLITUDE_THRESHOLD; + + if (left_leg_large && right_leg_large) { + ESP_LOGW(TAG, "检测到左右腿同时大幅度振荡,限制右腿振幅"); + amplitude[RIGHT_LEG] = 0; // 禁止右腿振荡 + } + if (left_foot_large && right_foot_large) { + ESP_LOGW(TAG, "检测到左右脚同时大幅度振荡,限制右脚振幅"); + amplitude[RIGHT_FOOT] = 0; // 禁止右脚振荡 + } + + // 读取相位差(短键名 "ph",单位为度,转换为弧度) + cJSON* phase_item = cJSON_GetObjectItem(osc_item, "ph"); + if (cJSON_IsObject(phase_item)) { + for (int j = 0; j < SERVO_COUNT; j++) { + cJSON* phase_value = cJSON_GetObjectItem(phase_item, servo_names[j]); + if (cJSON_IsNumber(phase_value)) { + // 将度数转换为弧度 + phase_diff[j] = phase_value->valuedouble * 3.141592653589793 / 180.0; + } + } + } + + // 读取周期(短键名 "p"),范围100-3000毫秒 + cJSON* period_item = cJSON_GetObjectItem(osc_item, "p"); + if (cJSON_IsNumber(period_item)) { + period = period_item->valueint; + if (period < 100) period = 100; + if (period > 3000) period = 3000; // 与描述一致,限制3000毫秒 + } + + // 读取周期数(短键名 "c"),范围0.1-20.0 + cJSON* steps_item = cJSON_GetObjectItem(osc_item, "c"); + if (cJSON_IsNumber(steps_item)) { + steps = (float)steps_item->valuedouble; + if (steps < 0.1) steps = 0.1; + if (steps > 20.0) steps = 20.0; // 与描述一致,限制20.0 + } + + // 执行振荡 - 使用Execute2,以绝对角度为中心 + ESP_LOGI(TAG, "执行振荡动作%d: period=%d, steps=%.1f", i, period, steps); + controller->otto_.Execute2(amplitude, center_angle, period, phase_diff, steps); + + // 振荡后更新位置(使用center_angle作为最终位置) + for (int j = 0; j < SERVO_COUNT; j++) { + current_positions[j] = center_angle[j]; + } + } else { + // 普通移动模式 + // 从当前位置数组复制,保持未指定的舵机位置 + int servo_target[SERVO_COUNT]; + for (int j = 0; j < SERVO_COUNT; j++) { + servo_target[j] = current_positions[j]; + } + + // 从JSON中读取舵机位置(短键名 "s") + cJSON* servos_item = cJSON_GetObjectItem(action_item, "s"); + if (cJSON_IsObject(servos_item)) { + // 短键名:ll/rl/lf/rf/lh/rh + const char* servo_names[] = {"ll", "rl", "lf", "rf", "lh", "rh"}; + + for (int j = 0; j < SERVO_COUNT; j++) { + cJSON* servo_value = cJSON_GetObjectItem(servos_item, servo_names[j]); + if (cJSON_IsNumber(servo_value)) { + int position = servo_value->valueint; + // 限制位置范围在0-180度 + if (position >= 0 && position <= 180) { + servo_target[j] = position; + } + } + } + } + + // 安全检查:防止左右腿脚同时做大幅度动作 + const int LARGE_MOVEMENT_THRESHOLD = 40; // 大幅度动作阈值:40度 + bool left_leg_large = abs(servo_target[LEFT_LEG] - current_positions[LEFT_LEG]) >= LARGE_MOVEMENT_THRESHOLD; + bool right_leg_large = abs(servo_target[RIGHT_LEG] - current_positions[RIGHT_LEG]) >= LARGE_MOVEMENT_THRESHOLD; + bool left_foot_large = abs(servo_target[LEFT_FOOT] - current_positions[LEFT_FOOT]) >= LARGE_MOVEMENT_THRESHOLD; + bool right_foot_large = abs(servo_target[RIGHT_FOOT] - current_positions[RIGHT_FOOT]) >= LARGE_MOVEMENT_THRESHOLD; + + if (left_leg_large && right_leg_large) { + ESP_LOGW(TAG, "检测到左右腿同时大幅度动作,限制右腿动作"); + // 保持右腿在原位置 + servo_target[RIGHT_LEG] = current_positions[RIGHT_LEG]; + } + if (left_foot_large && right_foot_large) { + ESP_LOGW(TAG, "检测到左右脚同时大幅度动作,限制右脚动作"); + // 保持右脚在原位置 + servo_target[RIGHT_FOOT] = current_positions[RIGHT_FOOT]; + } + + // 获取移动速度(短键名 "v",默认1000毫秒) + int speed = 1000; + cJSON* speed_item = cJSON_GetObjectItem(action_item, "v"); + if (cJSON_IsNumber(speed_item)) { + speed = speed_item->valueint; + if (speed < 100) speed = 100; // 最小100毫秒 + if (speed > 3000) speed = 3000; // 最大3000毫秒 + } + + // 执行舵机移动 + ESP_LOGI(TAG, "执行动作%d: ll=%d, rl=%d, lf=%d, rf=%d, v=%d", + i, servo_target[LEFT_LEG], servo_target[RIGHT_LEG], + servo_target[LEFT_FOOT], servo_target[RIGHT_FOOT], speed); + controller->otto_.MoveServos(speed, servo_target); + + // 更新当前位置数组,用于下一个动作 + for (int j = 0; j < SERVO_COUNT; j++) { + current_positions[j] = servo_target[j]; + } + } + + // 获取动作后的延迟时间(短键名 "d") + int delay_after = 0; + cJSON* delay_item = cJSON_GetObjectItem(action_item, "d"); + if (cJSON_IsNumber(delay_item)) { + delay_after = delay_item->valueint; + if (delay_after < 0) delay_after = 0; + } + + // 动作后的延迟(最后一个动作后不延迟) + if (delay_after > 0 && i < array_size - 1) { + ESP_LOGI(TAG, "动作%d执行完成,延迟%d毫秒", i, delay_after); + vTaskDelay(pdMS_TO_TICKS(delay_after)); + } + } + } + + // 序列执行完成后的延迟(用于序列之间的停顿) + if (sequence_delay > 0) { + // 检查队列中是否还有待执行的序列 + UBaseType_t queue_count = uxQueueMessagesWaiting(controller->action_queue_); + if (queue_count > 0) { + ESP_LOGI(TAG, "序列执行完成,延迟%d毫秒后执行下一个序列(队列中还有%d个序列)", + sequence_delay, queue_count); + vTaskDelay(pdMS_TO_TICKS(sequence_delay)); + } + } + // 释放JSON内存 + cJSON_Delete(json); + } else { + ESP_LOGE(TAG, "舵机序列格式错误: 'a'不是数组"); + cJSON_Delete(json); } - break; - case ACTION_HANDS_DOWN: - if (controller->has_hands_) { - controller->otto_.HandsDown(params.speed, params.direction); + } else { + // 获取cJSON的错误信息 + const char* error_ptr = cJSON_GetErrorPtr(); + int json_len = strlen(params.servo_sequence_json); + ESP_LOGE(TAG, "解析舵机序列JSON失败,长度=%d,错误位置: %s", json_len, + error_ptr ? error_ptr : "未知"); + ESP_LOGE(TAG, "JSON内容: %s", params.servo_sequence_json); + } + } else { + // 执行预定义动作 + switch (params.action_type) { + case ACTION_WALK: + controller->otto_.Walk(params.steps, params.speed, params.direction, + params.amount); + break; + case ACTION_TURN: + controller->otto_.Turn(params.steps, params.speed, params.direction, + params.amount); + break; + case ACTION_JUMP: + controller->otto_.Jump(params.steps, params.speed); + break; + case ACTION_SWING: + controller->otto_.Swing(params.steps, params.speed, params.amount); + break; + case ACTION_MOONWALK: + controller->otto_.Moonwalker(params.steps, params.speed, params.amount, + params.direction); + break; + case ACTION_BEND: + controller->otto_.Bend(params.steps, params.speed, params.direction); + break; + case ACTION_SHAKE_LEG: + controller->otto_.ShakeLeg(params.steps, params.speed, params.direction); + break; + case ACTION_SIT: + controller->otto_.Sit(); + break; + case ACTION_RADIO_CALISTHENICS: + if (controller->has_hands_) { + controller->otto_.RadioCalisthenics(); + } + break; + case ACTION_MAGIC_CIRCLE: + if (controller->has_hands_) { + controller->otto_.MagicCircle(); + } + break; + case ACTION_SHOWCASE: + controller->otto_.Showcase(); + break; + case ACTION_UPDOWN: + controller->otto_.UpDown(params.steps, params.speed, params.amount); + break; + case ACTION_TIPTOE_SWING: + controller->otto_.TiptoeSwing(params.steps, params.speed, params.amount); + break; + case ACTION_JITTER: + controller->otto_.Jitter(params.steps, params.speed, params.amount); + break; + case ACTION_ASCENDING_TURN: + controller->otto_.AscendingTurn(params.steps, params.speed, params.amount); + break; + case ACTION_CRUSAITO: + controller->otto_.Crusaito(params.steps, params.speed, params.amount, + params.direction); + break; + case ACTION_FLAPPING: + controller->otto_.Flapping(params.steps, params.speed, params.amount, + params.direction); + break; + case ACTION_WHIRLWIND_LEG: + controller->otto_.WhirlwindLeg(params.steps, params.speed, params.amount); + break; + case ACTION_HANDS_UP: + if (controller->has_hands_) { + controller->otto_.HandsUp(params.speed, params.direction); + } + break; + case ACTION_HANDS_DOWN: + if (controller->has_hands_) { + controller->otto_.HandsDown(params.speed, params.direction); + } + break; + case ACTION_HAND_WAVE: + if (controller->has_hands_) { + controller->otto_.HandWave( params.direction); + } + break; + case ACTION_WINDMILL: + if (controller->has_hands_) { + controller->otto_.Windmill(params.steps, params.speed, params.amount); + } + break; + case ACTION_TAKEOFF: + if (controller->has_hands_) { + controller->otto_.Takeoff(params.steps, params.speed, params.amount); + } + break; + case ACTION_FITNESS: + if (controller->has_hands_) { + controller->otto_.Fitness(params.steps, params.speed, params.amount); + } + break; + case ACTION_GREETING: + if (controller->has_hands_) { + controller->otto_.Greeting(params.direction, params.steps); + } + break; + case ACTION_SHY: + if (controller->has_hands_) { + controller->otto_.Shy(params.direction, params.steps); + } + break; + case ACTION_HOME: + controller->otto_.Home(true); + break; + } + if(params.action_type != ACTION_SIT){ + if (params.action_type != ACTION_HOME && params.action_type != ACTION_SERVO_SEQUENCE) { + controller->otto_.Home(params.action_type != ACTION_HANDS_UP); } - break; - case ACTION_HAND_WAVE: - if (controller->has_hands_) { - controller->otto_.HandWave(params.speed, params.direction); - } - break; - case ACTION_HOME: - controller->otto_.Home(params.direction == 1); - break; - } - if (params.action_type != ACTION_HOME) { - controller->otto_.Home(params.action_type < ACTION_HANDS_UP); + } } controller->is_action_in_progress_ = false; vTaskDelay(pdMS_TO_TICKS(20)); @@ -145,15 +440,52 @@ private: void QueueAction(int action_type, int steps, int speed, int direction, int amount) { // 检查手部动作 - if ((action_type >= ACTION_HANDS_UP && action_type <= ACTION_HAND_WAVE) && !has_hands_) { - ESP_LOGW(TAG, "尝试执行手部动作,但机器人没有配置手部舵机"); - return; + if ((action_type >= ACTION_HANDS_UP && action_type <= ACTION_HAND_WAVE) || + (action_type == ACTION_WINDMILL) || (action_type == ACTION_TAKEOFF) || + (action_type == ACTION_FITNESS) || (action_type == ACTION_GREETING) || + (action_type == ACTION_SHY) || (action_type == ACTION_RADIO_CALISTHENICS) || + (action_type == ACTION_MAGIC_CIRCLE)) { + if (!has_hands_) { + ESP_LOGW(TAG, "尝试执行手部动作,但机器人没有配置手部舵机"); + return; + } } ESP_LOGI(TAG, "动作控制: 类型=%d, 步数=%d, 速度=%d, 方向=%d, 幅度=%d", action_type, steps, speed, direction, amount); - OttoActionParams params = {action_type, steps, speed, direction, amount}; + OttoActionParams params = {action_type, steps, speed, direction, amount, ""}; + xQueueSend(action_queue_, ¶ms, portMAX_DELAY); + StartActionTaskIfNeeded(); + } + + void QueueServoSequence(const char* servo_sequence_json) { + if (servo_sequence_json == nullptr) { + ESP_LOGE(TAG, "序列JSON为空"); + return; + } + + int input_len = strlen(servo_sequence_json); + const int buffer_size = 512; // servo_sequence_json数组大小 + ESP_LOGI(TAG, "队列舵机序列,输入长度=%d,缓冲区大小=%d", input_len, buffer_size); + + if (input_len >= buffer_size) { + ESP_LOGE(TAG, "JSON字符串太长!输入长度=%d,最大允许=%d", input_len, buffer_size - 1); + return; + } + + if (input_len == 0) { + ESP_LOGW(TAG, "序列JSON为空字符串"); + return; + } + + OttoActionParams params = {ACTION_SERVO_SEQUENCE, 0, 0, 0, 0, ""}; + // 复制JSON字符串到结构体中(限制长度) + strncpy(params.servo_sequence_json, servo_sequence_json, sizeof(params.servo_sequence_json) - 1); + params.servo_sequence_json[sizeof(params.servo_sequence_json) - 1] = '\0'; + + ESP_LOGD(TAG, "序列已加入队列: %s", params.servo_sequence_json); + xQueueSend(action_queue_, ¶ms, portMAX_DELAY); StartActionTaskIfNeeded(); } @@ -299,6 +631,22 @@ public: return true; }); + mcp_server.AddTool("self.otto.sit", + "坐下。不需要参数", + PropertyList(), + [this](const PropertyList& properties) -> ReturnValue { + QueueAction(ACTION_SIT, 1, 0, 0, 0); + return true; + }); + + mcp_server.AddTool("self.otto.showcase", + "展示动作。串联执行多个动作:往前走3步、挥挥手、跳舞(广播体操)、太空步、摇摆、起飞、健身、往后走3步。不需要参数", + PropertyList(), + [this](const PropertyList& properties) -> ReturnValue { + QueueAction(ACTION_SHOWCASE, 1, 0, 0, 0); + return true; + }); + mcp_server.AddTool("self.otto.updown", "上下运动。steps: 上下运动次数(1-100); speed: " "运动速度(500-1500,数值越小越快); amount: 运动幅度(0-170度)", @@ -313,6 +661,21 @@ public: return true; }); + mcp_server.AddTool("self.otto.whirlwind_leg", + "旋风腿。" + "steps: 动作次数(3-100); speed: 动作速度(100-1000,数值越小越快,建议300); " + "amplitude: 踢腿幅度(20-40度)", + PropertyList({Property("steps", kPropertyTypeInteger, 3, 3, 100), + Property("speed", kPropertyTypeInteger, 300, 100, 1000), + Property("amplitude", kPropertyTypeInteger, 30, 20, 40)}), + [this](const PropertyList& properties) -> ReturnValue { + int steps = properties["steps"].value(); + int speed = properties["speed"].value(); + int amplitude = properties["amplitude"].value(); + QueueAction(ACTION_WHIRLWIND_LEG, steps, speed, 0, amplitude); + return true; + }); + // 手部动作(仅在有手部舵机时可用) if (has_hands_) { mcp_server.AddTool( @@ -343,20 +706,141 @@ public: mcp_server.AddTool( "self.otto.hand_wave", - "挥手。speed: 挥手速度(500-1500,数值越小越快); direction: 手部选择(1=左手, " - "-1=右手, 0=双手)", - PropertyList({Property("speed", kPropertyTypeInteger, 1000, 500, 1500), - Property("direction", kPropertyTypeInteger, 1, -1, 1)}), + "挥手。direction: 手部选择(1=左手,-1=右手,0=双手)", + PropertyList({Property("direction", kPropertyTypeInteger, 1, -1, 1)}), [this](const PropertyList& properties) -> ReturnValue { + int direction = properties["direction"].value(); + QueueAction(ACTION_HAND_WAVE, 1, 0, 0, direction); + return true; + }); + + mcp_server.AddTool( + "self.otto.windmill", + "大风车。steps: 动作次数(3-100); " + "speed: 动作周期(300-2000毫秒,数值越小越快); amplitude: 振荡幅度(50-90度)", + PropertyList({Property("steps", kPropertyTypeInteger, 6, 3, 100), + Property("speed", kPropertyTypeInteger, 500, 300, 2000), + Property("amplitude", kPropertyTypeInteger, 70, 50, 90)}), + [this](const PropertyList& properties) -> ReturnValue { + int steps = properties["steps"].value(); int speed = properties["speed"].value(); - int direction = properties["direction"].value(); - QueueAction(ACTION_HAND_WAVE, 1, speed, direction, 0); + int amplitude = properties["amplitude"].value(); + QueueAction(ACTION_WINDMILL, steps, speed, 0, amplitude); return true; }); + + mcp_server.AddTool( + "self.otto.takeoff", + "起飞。双手在90度位置同相快速振荡,模拟起飞动作。steps: 动作次数(5-100); " + "speed: 动作周期(200-600毫秒,数值越小越快,建议300); amplitude: 振荡幅度(20-60度)", + PropertyList({Property("steps", kPropertyTypeInteger, 5, 5, 100), + Property("speed", kPropertyTypeInteger, 300, 200, 600), + Property("amplitude", kPropertyTypeInteger, 40, 20, 60)}), + [this](const PropertyList& properties) -> ReturnValue { + int steps = properties["steps"].value(); + int speed = properties["speed"].value(); + int amplitude = properties["amplitude"].value(); + QueueAction(ACTION_TAKEOFF, steps, speed, 0, amplitude); + return true; + }); + + mcp_server.AddTool( + "self.otto.fitness", + "健身。steps: 动作次数(3-100); speed: 动作速度(500-2000毫秒,数值越小越快); amplitude: 振荡幅度(10-50度)", + PropertyList({Property("steps", kPropertyTypeInteger, 5, 3, 100), + Property("speed", kPropertyTypeInteger, 1000, 500, 2000), + Property("amplitude", kPropertyTypeInteger, 25, 10, 50)}), + [this](const PropertyList& properties) -> ReturnValue { + int steps = properties["steps"].value(); + int speed = properties["speed"].value(); + int amplitude = properties["amplitude"].value(); + QueueAction(ACTION_FITNESS, steps, speed, 0, amplitude); + return true; + }); + + mcp_server.AddTool( + "self.otto.greeting", + "打招呼。direction: 手部选择(1=左手, -1=右手); steps: 动作次数(3-100)", + PropertyList({Property("direction", kPropertyTypeInteger, 1, -1, 1), + Property("steps", kPropertyTypeInteger, 5, 3, 100)}), + [this](const PropertyList& properties) -> ReturnValue { + int direction = properties["direction"].value(); + int steps = properties["steps"].value(); + QueueAction(ACTION_GREETING, steps, 0, direction, 0); + return true; + }); + + mcp_server.AddTool( + "self.otto.shy", + "害羞。direction: 方向(1=左, -1=右); steps: 动作次数(3-100)", + PropertyList({Property("direction", kPropertyTypeInteger, 1, -1, 1), + Property("steps", kPropertyTypeInteger, 5, 3, 100)}), + [this](const PropertyList& properties) -> ReturnValue { + int direction = properties["direction"].value(); + int steps = properties["steps"].value(); + QueueAction(ACTION_SHY, steps, 0, direction, 0); + return true; + }); + + mcp_server.AddTool("self.otto.radio_calisthenics", + "广播体操。不需要参数", + PropertyList(), + [this](const PropertyList& properties) -> ReturnValue { + QueueAction(ACTION_RADIO_CALISTHENICS, 1, 0, 0, 0); + return true; + }); + + mcp_server.AddTool("self.otto.magic_circle", + "爱的魔力转圈圈。不需要参数", + PropertyList(), + [this](const PropertyList& properties) -> ReturnValue { + QueueAction(ACTION_MAGIC_CIRCLE, 1, 0, 0, 0); + return true; + }); } + // 舵机序列工具(支持分段发送,每次发送一个序列,自动排队执行) + mcp_server.AddTool( + "self.otto.servo_sequences", + "控制每个舵机实现自主动作编程。支持分段发送序列:AI可以连续多次调用此工具,每次发送一个短序列,系统会自动排队按顺序执行。支持普通移动和振荡器两种模式。" + "机器人结构:双手可上下摆动,双腿可内收外展,双脚可上下翻转。" + "舵机说明:" + "ll(左腿):内收外展,0度=完全外展,90度=中立,180度=完全内收;" + "rl(右腿):内收外展,0度=完全内收,90度=中立,180度=完全外展;" + "lf(左脚):上下翻转,0度=完全向上,90度=水平,180度=完全向下;" + "rf(右脚):上下翻转,0度=完全向下,90度=水平,180度=完全向上;" + "lh(左手):上下摆动,0度=完全向下,90度=水平,180度=完全向上;" + "rh(右手):上下摆动,0度=完全向上,90度=水平,180度=完全向下;" + "sequence: 单个序列对象,包含'a'动作数组,顶层可选参数:" + "'d'(序列执行完成后延迟毫秒数,用于序列之间的停顿)。" + "每个动作对象包含:" + "普通模式:'s'舵机位置对象(键名:ll/rl/lf/rf/lh/rh,值:0-180度),'v'移动速度100-3000毫秒(默认1000),'d'动作后延迟毫秒数(默认0);" + "振荡模式:'osc'振荡器对象,包含'a'振幅对象(各舵机振幅10-90度,默认20度),'o'中心角度对象(各舵机振荡中心绝对角度0-180度,默认90度),'ph'相位差对象(各舵机相位差,度,0-360度,默认0度),'p'周期100-3000毫秒(默认500),'c'周期数0.1-20.0(默认5.0);" + "使用方式:AI可以连续多次调用此工具,每次发送一个序列,系统会自动排队按顺序执行。" + "重要说明:左右腿脚震荡的时候,有一只脚必须在90度,否则会损坏机器人,如果发送多个序列(序列数>1),完成所有序列后需要复位时,AI应该最后单独调用self.otto.home工具进行复位,不要在序列中设置复位参数。" + "示例:发送3个序列,最后调用复位:" + "第1次调用{\"sequence\":\"{\\\"a\\\":[{\\\"s\\\":{\\\"ll\\\":100},\\\"v\\\":1000}],\\\"d\\\":500}\"}," + "第2次调用{\"sequence\":\"{\\\"a\\\":[{\\\"s\\\":{\\\"ll\\\":90},\\\"v\\\":800}],\\\"d\\\":500}\"}," + "第3次调用{\"sequence\":\"{\\\"a\\\":[{\\\"s\\\":{\\\"ll\\\":80},\\\"v\\\":800}]}\"}," + "最后调用self.otto.home工具进行复位。", + PropertyList({Property("sequence", kPropertyTypeString, + "{\"a\":[{\"s\":{\"ll\":90,\"rl\":90},\"v\":1000}]}")}), + [this](const PropertyList& properties) -> ReturnValue { + std::string sequence = properties["sequence"].value(); + // 检查是否是JSON对象(可能是字符串格式或已解析的对象) + // 如果sequence是JSON字符串,直接使用;如果是对象字符串,也需要使用 + QueueServoSequence(sequence.c_str()); + return true; + }); + // 系统工具 - mcp_server.AddTool("self.otto.stop", "立即停止", PropertyList(), + mcp_server.AddTool("self.otto.home", "复位机器人到初始位置", PropertyList(), + [this](const PropertyList& properties) -> ReturnValue { + QueueAction(ACTION_HOME, 1, 1000, 1, 0); + return true; + }); + + mcp_server.AddTool("self.otto.stop", "立即停止所有动作并复位", PropertyList(), [this](const PropertyList& properties) -> ReturnValue { if (action_task_handle_ != nullptr) { vTaskDelete(action_task_handle_); diff --git a/main/boards/otto-robot/otto_emoji_display.cc b/main/boards/otto-robot/otto_emoji_display.cc index c6477fcf..51bb71a2 100644 --- a/main/boards/otto-robot/otto_emoji_display.cc +++ b/main/boards/otto-robot/otto_emoji_display.cc @@ -88,7 +88,7 @@ void OttoEmojiDisplay::SetupChatLabel() { chat_message_label_ = lv_label_create(container_); lv_label_set_text(chat_message_label_, ""); lv_obj_set_width(chat_message_label_, width_ * 0.9); // 限制宽度为屏幕宽度的 90% - lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_WRAP); // 设置为自动换行模式 + lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); // 设置文本居中对齐 lv_obj_set_style_text_color(chat_message_label_, lv_color_white(), 0); SetTheme(LvglThemeManager::GetInstance().GetTheme("dark")); diff --git a/main/boards/otto-robot/otto_movements.cc b/main/boards/otto-robot/otto_movements.cc index 5dee127c..9d11f129 100644 --- a/main/boards/otto-robot/otto_movements.cc +++ b/main/boards/otto-robot/otto_movements.cc @@ -2,6 +2,7 @@ #include +#include "freertos/idf_additions.h" #include "oscillator.h" static const char* TAG = "OttoMovements"; @@ -198,6 +199,39 @@ void Otto::Execute(int amplitude[SERVO_COUNT], int offset[SERVO_COUNT], int peri vTaskDelay(pdMS_TO_TICKS(10)); } +//--------------------------------------------------------- +//-- Execute2: 使用绝对角度作为振荡中心 +//-- Parameters: +//-- amplitude: 振幅数组(每个舵机的振荡幅度) +//-- center_angle: 绝对角度数组(0-180度),作为振荡中心位置 +//-- period: 周期(毫秒) +//-- phase_diff: 相位差数组(弧度) +//-- steps: 步数/周期数(可为小数) +//--------------------------------------------------------- +void Otto::Execute2(int amplitude[SERVO_COUNT], int center_angle[SERVO_COUNT], int period, + double phase_diff[SERVO_COUNT], float steps = 1.0) { + if (GetRestState() == true) { + SetRestState(false); + } + + // 将绝对角度转换为offset(offset = center_angle - 90) + int offset[SERVO_COUNT]; + for (int i = 0; i < SERVO_COUNT; i++) { + offset[i] = center_angle[i] - 90; + } + + int cycles = (int)steps; + + //-- Execute complete cycles + if (cycles >= 1) + for (int i = 0; i < cycles; i++) + OscillateServos(amplitude, offset, period, phase_diff); + + //-- Execute the final not complete cycle + OscillateServos(amplitude, offset, period, phase_diff, (float)steps - cycles); + vTaskDelay(pdMS_TO_TICKS(10)); +} + /////////////////////////////////////////////////////////////////// //-- HOME = Otto at rest position -------------------------------// /////////////////////////////////////////////////////////////////// @@ -224,7 +258,7 @@ void Otto::Home(bool hands_down) { } } - MoveServos(500, homes); + MoveServos(700, homes); is_otto_resting_ = true; } @@ -389,7 +423,7 @@ void Otto::ShakeLeg(int steps, int period, int dir) { int homes[SERVO_COUNT] = {90, 90, 90, 90, HAND_HOME_POSITION, 180 - HAND_HOME_POSITION}; // Changes in the parameters if left leg is chosen - if (dir == 1) { + if (dir == LEFT) { shake_leg1[2] = 180 - 35; shake_leg1[3] = 180 - 58; shake_leg2[2] = 180 - 120; @@ -420,6 +454,14 @@ void Otto::ShakeLeg(int steps, int period, int dir) { vTaskDelay(pdMS_TO_TICKS(period)); } +//--------------------------------------------------------- +//-- Otto movement: Sit (坐下) +//--------------------------------------------------------- +void Otto::Sit() { + int target[SERVO_COUNT] = {120, 60, 0, 180, 45, 135}; + MoveServos(600, target); +} + //--------------------------------------------------------- //-- Otto movement: up & down //-- Parameters: @@ -588,6 +630,29 @@ void Otto::Flapping(float steps, int period, int height, int dir) { Execute(A, O, period, phase_diff, steps); } +//--------------------------------------------------------- +//-- Otto gait: WhirlwindLeg (旋风腿) +//-- Parameters: +//-- steps: Number of steps +//-- period: Period (建议100-800毫秒) +//-- amplitude: amplitude (Values between 20 - 40) +//--------------------------------------------------------- +void Otto::WhirlwindLeg(float steps, int period, int amplitude) { + + + int target[SERVO_COUNT] = {90, 90, 180, 90, 45, 20}; + MoveServos(100, target); + target[RIGHT_FOOT] = 160; + MoveServos(500, target); + vTaskDelay(pdMS_TO_TICKS(1000)); + + int C[SERVO_COUNT] = {90, 90, 180, 160, 45, 20}; + int A[SERVO_COUNT] = {amplitude, 0, 0, 0, amplitude, 0}; + double phase_diff[SERVO_COUNT] = {DEG2RAD(20), 0, 0, 0, DEG2RAD(20), 0}; + Execute2(A, C, period, phase_diff, steps); + +} + //--------------------------------------------------------- //-- 手部动作: 举手 //-- Parameters: @@ -599,16 +664,15 @@ void Otto::HandsUp(int period, int dir) { return; } - int initial[SERVO_COUNT] = {90, 90, 90, 90, HAND_HOME_POSITION, 180 - HAND_HOME_POSITION}; int target[SERVO_COUNT] = {90, 90, 90, 90, HAND_HOME_POSITION, 180 - HAND_HOME_POSITION}; if (dir == 0) { target[LEFT_HAND] = 170; target[RIGHT_HAND] = 10; - } else if (dir == 1) { + } else if (dir == LEFT) { target[LEFT_HAND] = 170; target[RIGHT_HAND] = servo_[RIGHT_HAND].GetPosition(); - } else if (dir == -1) { + } else if (dir == RIGHT) { target[RIGHT_HAND] = 10; target[LEFT_HAND] = servo_[LEFT_HAND].GetPosition(); } @@ -629,9 +693,9 @@ void Otto::HandsDown(int period, int dir) { int target[SERVO_COUNT] = {90, 90, 90, 90, HAND_HOME_POSITION, 180 - HAND_HOME_POSITION}; - if (dir == 1) { + if (dir == LEFT) { target[RIGHT_HAND] = servo_[RIGHT_HAND].GetPosition(); - } else if (dir == -1) { + } else if (dir == RIGHT) { target[LEFT_HAND] = servo_[LEFT_HAND].GetPosition(); } @@ -639,111 +703,248 @@ void Otto::HandsDown(int period, int dir) { } //--------------------------------------------------------- -//-- 手部动作: 挥手 +//-- 手部动作: 挥手 //-- Parameters: -//-- period: 动作周期 -//-- dir: 方向 LEFT/RIGHT/BOTH +//-- dir: 方向 LEFT/RIGHT/BOTH //--------------------------------------------------------- -void Otto::HandWave(int period, int dir) { +void Otto::HandWave(int dir) { + if (!has_hands_) { + return; + } + if (dir == LEFT) { + int center_angle[SERVO_COUNT] = {90, 90, 90, 90, 160, 135}; + int A[SERVO_COUNT] = {0, 0, 0, 0, 20, 0}; + double phase_diff[SERVO_COUNT] = {0, 0, 0, 0, DEG2RAD(90), 0}; + Execute2(A, center_angle, 300, phase_diff, 5); + } + else if (dir == RIGHT) { + int center_angle[SERVO_COUNT] = {90, 90, 90, 90, 45, 20}; + int A[SERVO_COUNT] = {0, 0, 0, 0, 0, 20}; + double phase_diff[SERVO_COUNT] = {0, 0, 0, 0, 0, DEG2RAD(90)}; + Execute2(A, center_angle, 300, phase_diff, 5); + } + else { + int center_angle[SERVO_COUNT] = {90, 90, 90, 90, 160, 20}; + int A[SERVO_COUNT] = {0, 0, 0, 0, 20, 20}; + double phase_diff[SERVO_COUNT] = {0, 0, 0, 0, DEG2RAD(90), DEG2RAD(90)}; + Execute2(A, center_angle, 300, phase_diff, 5); + } +} + + +//--------------------------------------------------------- +//-- 手部动作: 大风车 +//-- Parameters: +//-- steps: 动作次数 +//-- period: 动作周期(毫秒) +//-- amplitude: 振荡幅度(度) +//--------------------------------------------------------- +void Otto::Windmill(float steps, int period, int amplitude) { if (!has_hands_) { return; } - if (dir == BOTH) { - HandWaveBoth(period); - return; - } - - int servo_index = (dir == LEFT) ? LEFT_HAND : RIGHT_HAND; - - int current_positions[SERVO_COUNT]; - for (int i = 0; i < SERVO_COUNT; i++) { - if (servo_pins_[i] != -1) { - current_positions[i] = servo_[i].GetPosition(); - } else { - current_positions[i] = 90; - } - } - - int position; - if (servo_index == LEFT_HAND) { - position = 170; - } else { - position = 10; - } - - current_positions[servo_index] = position; - MoveServos(300, current_positions); - vTaskDelay(pdMS_TO_TICKS(300)); - - // 左右摆动5次 - for (int i = 0; i < 5; i++) { - if (servo_index == LEFT_HAND) { - current_positions[servo_index] = position - 30; - MoveServos(period / 10, current_positions); - vTaskDelay(pdMS_TO_TICKS(period / 10)); - current_positions[servo_index] = position + 30; - MoveServos(period / 10, current_positions); - } else { - current_positions[servo_index] = position + 30; - MoveServos(period / 10, current_positions); - vTaskDelay(pdMS_TO_TICKS(period / 10)); - current_positions[servo_index] = position - 30; - MoveServos(period / 10, current_positions); - } - vTaskDelay(pdMS_TO_TICKS(period / 10)); - } - - if (servo_index == LEFT_HAND) { - current_positions[servo_index] = HAND_HOME_POSITION; - } else { - current_positions[servo_index] = 180 - HAND_HOME_POSITION; - } - MoveServos(300, current_positions); + int center_angle[SERVO_COUNT] = {90, 90, 90, 90, 90, 90}; + int A[SERVO_COUNT] = {0, 0, 0, 0, amplitude, amplitude}; + double phase_diff[SERVO_COUNT] = {0, 0, 0, 0, DEG2RAD(90), DEG2RAD(90)}; + Execute2(A, center_angle, period, phase_diff, steps); } //--------------------------------------------------------- -//-- 手部动作: 双手同时挥手 +//-- 手部动作: 起飞 //-- Parameters: -//-- period: 动作周期 +//-- steps: 动作次数 +//-- period: 动作周期(毫秒),数值越小速度越快 +//-- amplitude: 振荡幅度(度) //--------------------------------------------------------- -void Otto::HandWaveBoth(int period) { +void Otto::Takeoff(float steps, int period, int amplitude) { if (!has_hands_) { return; } - int current_positions[SERVO_COUNT]; - for (int i = 0; i < SERVO_COUNT; i++) { - if (servo_pins_[i] != -1) { - current_positions[i] = servo_[i].GetPosition(); - } else { - current_positions[i] = 90; - } + Home(true); + + int center_angle[SERVO_COUNT] = {90, 90, 90, 90, 90, 90}; + int A[SERVO_COUNT] = {0, 0, 0, 0, amplitude, amplitude}; + double phase_diff[SERVO_COUNT] = {0, 0, 0, 0, DEG2RAD(90), DEG2RAD(-90)}; + Execute2(A, center_angle, period, phase_diff, steps); +} + +//--------------------------------------------------------- +//-- 手部动作: 健身 +//-- Parameters: +//-- steps: 动作次数 +//-- period: 动作周期(毫秒) +//-- amplitude: 振荡幅度(度) +//--------------------------------------------------------- +void Otto::Fitness(float steps, int period, int amplitude) { + if (!has_hands_) { + return; + } + int target[SERVO_COUNT] = {90, 90, 90, 0, 160, 135}; + MoveServos(100, target); + target[LEFT_FOOT] = 20; + MoveServos(400, target); + vTaskDelay(pdMS_TO_TICKS(2000)); + + int C[SERVO_COUNT] = {90, 90, 20, 90, 160, 135}; + int A[SERVO_COUNT] = {0, 0, 0, 0, 0, amplitude}; + double phase_diff[SERVO_COUNT] = {0, 0, 0, 0, 0, 0}; + Execute2(A, C, period, phase_diff, steps); + +} + +//--------------------------------------------------------- +//-- 手部动作: 打招呼 +//-- Parameters: +//-- dir: 方向 LEFT=左手, RIGHT=右手 +//-- steps: 动作次数 +//--------------------------------------------------------- +void Otto::Greeting(int dir, float steps) { + if (!has_hands_) { + return; + } + if (dir == LEFT) { + int target[SERVO_COUNT] = {90, 90, 150, 150, 45, 135}; + MoveServos(400, target); + int C[SERVO_COUNT] = {90, 90, 150, 150, 160, 135}; + int A[SERVO_COUNT] = {0, 0, 0, 0, 20, 0}; + double phase_diff[SERVO_COUNT] = {0, 0, 0, 0, 0, 0}; + Execute2(A, C, 300, phase_diff, steps); + } + else if (dir == RIGHT) { + int target[SERVO_COUNT] = {90, 90, 30, 30, 45, 135}; + MoveServos(400, target); + int C[SERVO_COUNT] = {90, 90, 30, 30, 45, 20}; + int A[SERVO_COUNT] = {0, 0, 0, 0, 0, 20}; + double phase_diff[SERVO_COUNT] = {0, 0, 0, 0, 0, 0}; + Execute2(A, C, 300, phase_diff, steps); } - int left_position = 170; - int right_position = 10; +} - current_positions[LEFT_HAND] = left_position; - current_positions[RIGHT_HAND] = right_position; - MoveServos(300, current_positions); - - // 左右摆动5次 - for (int i = 0; i < 5; i++) { - // 波浪向左 - current_positions[LEFT_HAND] = left_position - 30; - current_positions[RIGHT_HAND] = right_position + 30; - MoveServos(period / 10, current_positions); - - // 波浪向右 - current_positions[LEFT_HAND] = left_position + 30; - current_positions[RIGHT_HAND] = right_position - 30; - MoveServos(period / 10, current_positions); +//--------------------------------------------------------- +//-- 手部动作: 害羞 +//-- Parameters: +//-- dir: 方向 LEFT=左手, RIGHT=右手 +//-- steps: 动作次数 +//--------------------------------------------------------- +void Otto::Shy(int dir, float steps) { + if (!has_hands_) { + return; } - current_positions[LEFT_HAND] = HAND_HOME_POSITION; - current_positions[RIGHT_HAND] = 180 - HAND_HOME_POSITION; - MoveServos(300, current_positions); + if (dir == LEFT) { + int target[SERVO_COUNT] = {90, 90, 150, 150, 45, 135}; + MoveServos(400, target); + int C[SERVO_COUNT] = {90, 90, 150, 150, 45, 135}; + int A[SERVO_COUNT] = {0, 0, 0, 0, 20, 20}; + double phase_diff[SERVO_COUNT] = {0, 0, 0, 0, DEG2RAD(90), DEG2RAD(-90)}; + Execute2(A, C, 300, phase_diff, steps); + } + else if (dir == RIGHT) { + int target[SERVO_COUNT] = {90, 90, 30, 30, 45, 135}; + MoveServos(400, target); + int C[SERVO_COUNT] = {90, 90, 30, 30, 45, 135}; + int A[SERVO_COUNT] = {0, 0, 0, 0, 0, 20}; + double phase_diff[SERVO_COUNT] = {0, 0, 0, 0, DEG2RAD(90), DEG2RAD(-90)}; + Execute2(A, C, 300, phase_diff, steps); + } +} + +//--------------------------------------------------------- +//-- 手部动作: 广播体操 +//--------------------------------------------------------- +void Otto::RadioCalisthenics() { + if (!has_hands_) { + return; + } + + const int period = 1000; + const float steps = 8.0; + + int C1[SERVO_COUNT] = {90, 90, 90, 90, 145, 45}; + int A1[SERVO_COUNT] = {0, 0, 0, 0, 45, 45}; + double phase_diff1[SERVO_COUNT] = {0, 0, 0, 0, DEG2RAD(90), DEG2RAD(-90)}; + Execute2(A1, C1, period, phase_diff1, steps); + + int C2[SERVO_COUNT] = {90, 90, 115, 65, 90, 90}; + int A2[SERVO_COUNT] = {0, 0, 25, 25, 0, 0}; + double phase_diff2[SERVO_COUNT] = {0, 0, DEG2RAD(90), DEG2RAD(-90), 0, 0}; + Execute2(A2, C2, period, phase_diff2, steps); + + int C3[SERVO_COUNT] = {90, 90, 130, 130, 90, 90}; + int A3[SERVO_COUNT] = {0, 0, 0, 0, 20, 0}; + double phase_diff3[SERVO_COUNT] = {0, 0, 0, 0, 0, 0}; + Execute2(A3, C3, period, phase_diff3, steps); + + int C4[SERVO_COUNT] = {90, 90, 50, 50, 90, 90}; + int A4[SERVO_COUNT] = {0, 0, 0, 0, 0, 20}; + double phase_diff4[SERVO_COUNT] = {0, 0, 0, 0, 0, 0}; + Execute2(A4, C4, period, phase_diff4, steps); +} + +//--------------------------------------------------------- +//-- 手部动作: 爱的魔力转圈圈 +//--------------------------------------------------------- +void Otto::MagicCircle() { + if (!has_hands_) { + return; + } + + int A[SERVO_COUNT] = {30, 30, 30, 30, 50, 50}; + int O[SERVO_COUNT] = {0, 0, 5, -5, 0, 0}; + double phase_diff[SERVO_COUNT] = {0, 0, DEG2RAD(-90), DEG2RAD(-90), DEG2RAD(-90) , DEG2RAD(90)}; + + Execute(A, O, 700, phase_diff, 40); +} + +//--------------------------------------------------------- +//-- 展示动作:串联多个动作展示 +//--------------------------------------------------------- +void Otto::Showcase() { + if (GetRestState() == true) { + SetRestState(false); + } + + // 1. 往前走3步 + Walk(3, 1000, FORWARD, 50); + vTaskDelay(pdMS_TO_TICKS(500)); + + // 2. 挥挥手 + if (has_hands_) { + HandWave(LEFT); + vTaskDelay(pdMS_TO_TICKS(500)); + } + + // 3. 跳舞(使用广播体操) + if (has_hands_) { + RadioCalisthenics(); + vTaskDelay(pdMS_TO_TICKS(500)); + } + + // 4. 太空步 + Moonwalker(3, 900, 25, LEFT); + vTaskDelay(pdMS_TO_TICKS(500)); + + // 5. 摇摆 + Swing(3, 1000, 30); + vTaskDelay(pdMS_TO_TICKS(500)); + + // 6. 起飞 + if (has_hands_) { + Takeoff(5, 300, 40); + vTaskDelay(pdMS_TO_TICKS(500)); + } + + // 7. 健身 + if (has_hands_) { + Fitness(5, 1000, 25); + vTaskDelay(pdMS_TO_TICKS(500)); + } + + // 8. 往后走3步 + Walk(3, 1000, BACKWARD, 50); } void Otto::EnableServoLimit(int diff_limit) { diff --git a/main/boards/otto-robot/otto_movements.h b/main/boards/otto-robot/otto_movements.h index cab13a05..69b944e4 100644 --- a/main/boards/otto-robot/otto_movements.h +++ b/main/boards/otto-robot/otto_movements.h @@ -51,6 +51,8 @@ public: void MoveSingle(int position, int servo_number); void OscillateServos(int amplitude[SERVO_COUNT], int offset[SERVO_COUNT], int period, double phase_diff[SERVO_COUNT], float cycle); + void Execute2(int amplitude[SERVO_COUNT], int center_angle[SERVO_COUNT], int period, + double phase_diff[SERVO_COUNT], float steps); //-- HOME = Otto at rest position void Home(bool hands_down = true); @@ -64,6 +66,7 @@ public: void Turn(float steps = 4, int period = 2000, int dir = LEFT, int amount = 0); void Bend(int steps = 1, int period = 1400, int dir = LEFT); void ShakeLeg(int steps = 1, int period = 2000, int dir = RIGHT); + void Sit(); // 坐下 void UpDown(float steps = 1, int period = 1000, int height = 20); void Swing(float steps = 1, int period = 1000, int height = 20); @@ -74,12 +77,20 @@ public: void Moonwalker(float steps = 1, int period = 900, int height = 20, int dir = LEFT); void Crusaito(float steps = 1, int period = 900, int height = 20, int dir = FORWARD); void Flapping(float steps = 1, int period = 1000, int height = 20, int dir = FORWARD); + void WhirlwindLeg(float steps = 1, int period = 300, int amplitude = 30); // -- 手部动作 void HandsUp(int period = 1000, int dir = 0); // 双手举起 void HandsDown(int period = 1000, int dir = 0); // 双手放下 - void HandWave(int period = 1000, int dir = LEFT); // 挥手 - void HandWaveBoth(int period = 1000); // 双手同时挥手 + void HandWave(int dir = LEFT); // 挥手 + void Windmill(float steps = 10, int period = 500, int amplitude = 90); // 大风车 + void Takeoff(float steps = 5, int period = 300, int amplitude = 40); // 起飞 + void Fitness(float steps = 5, int period = 1000, int amplitude = 25); // 健身 + void Greeting(int dir = LEFT, float steps = 5); // 打招呼 + void Shy(int dir = LEFT, float steps = 5); // 害羞 + void RadioCalisthenics(); // 广播体操 + void MagicCircle(); // 爱的魔力转圈圈 + void Showcase(); // 展示动作(串联多个动作) // -- Servo limiter void EnableServoLimit(int speed_limit_degree_per_sec = SERVO_LIMIT_DEFAULT); @@ -100,6 +111,7 @@ private: void Execute(int amplitude[SERVO_COUNT], int offset[SERVO_COUNT], int period, double phase_diff[SERVO_COUNT], float steps); + }; #endif // __OTTO_MOVEMENTS_H__ \ No newline at end of file