feat(motion): smooth action ending and adaptive homing on otto/electron-bot (#2001)

* Enhance Otto Robot camera support by adding configuration for OV3660. Updated config.h to define camera types and GPIO settings, modified config.json to include new camera options, and refactored otto_robot.cc for improved camera detection and initialization logic.

* fix: 移除 OttoEmojiDisplay 构造函数中的 SetTheme 调用以修复 LoadProhibited 崩溃

Made-with: Cursor

* refactor: improve audio service error handling and codec timeout management

- Updated AudioService to prevent input task termination on read timeout, introducing a delay instead.
- Enhanced NoAudioCodec to implement a read timeout for I2S channel reads.
- Adjusted WebSocketControlServer to set a control port for improved socket management.
- Added manufacturer information to the config.json for waveshare ESP32-Touch-LCD-3.5.

* fix(otto): WebSocket direct clients not receiving MCP responses

When a browser connects directly to the WebSocket control server (port
8080) and sends a JSON-RPC request, the MCP response was routed through
Application::SendMcpMessage -> protocol_->SendMcpMessage, which sends it
to the cloud protocol channel. As a result, the direct WebSocket client
never received the response, while the WeChat mini-program could because
it communicates via the cloud.

Fix:
- Add BroadcastMessage() to WebSocketControlServer, using
  httpd_queue_work + httpd_ws_send_frame_async to asynchronously
  send responses back to all connected clients on port 8080
- Add RegisterMcpBroadcastCallback() to Application, allowing an
  additional MCP send callback to be registered; SendMcpMessage()
  now invokes it alongside the cloud protocol
- Register the broadcast callback in OttoRobot after the WebSocket
  server starts successfully

Also add WebSocket direct-connect API documentation to README.md
with complete JSON-RPC 2.0 command examples.

* fix(otto-robot): migrate camera backend and set safe dark default theme

- Migrate `otto-robot` camera backend from `EspVideo` to `Esp32Camera` to improve capture stability after reboot/power cycle.
- Keep runtime sensor detection for both OV2640 and OV3660, and rename PID macros with `OTTO_` prefix to avoid symbol conflicts.
- Configure camera output as `RGB565 + FRAMESIZE_240X240` to match the 240x240 display.
- Rotate OV2640 output by 180 degrees (`VFlip + HMirror`) for correct orientation.
- Simplify `otto-robot` camera sdkconfig options by keeping only:
  - `CONFIG_CAMERA_OV2640=y`
  - `CONFIG_CAMERA_OV3660=y`
- Move Otto default dark theme setup into `OttoEmojiDisplay::SetupUI()` (after base UI init) to avoid boot-time LVGL null-object crash caused by calling `SetTheme()` too early.

* feat(motion): smooth action ending and adaptive homing on otto/electron-bot

Improve servo motion transitions to reduce abrupt returns to home pose.
Replace linear interpolation with ease-out movement, make homing duration adaptive to angle delta, and skip intermediate homing when queued actions are pending to keep multi-action sequences fluid.
This commit is contained in:
小鹏
2026-05-25 20:58:05 +08:00
committed by GitHub
parent 36200942cc
commit ed871cdeef
4 changed files with 90 additions and 74 deletions

View File

@@ -95,6 +95,14 @@ private:
// 复位动作
controller->electron_bot_.Home(true);
}
if (params.action_type != ACTION_HOME) {
UBaseType_t pending_actions =
uxQueueMessagesWaiting(controller->action_queue_);
// 连续动作时跳过中间归位,避免动作衔接生硬
if (pending_actions == 0) {
controller->electron_bot_.Home(true);
}
}
controller->is_action_in_progress_ = false; // 动作执行完毕
}
vTaskDelay(pdMS_TO_TICKS(20));

View File

@@ -1,10 +1,18 @@
#include "movements.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include "oscillator.h"
namespace {
float EaseOutCubic(float t) {
float inv = 1.0f - t;
return 1.0f - inv * inv * inv;
}
} // namespace
Otto::Otto() {
is_otto_resting_ = false;
for (int i = 0; i < SERVO_COUNT; i++) {
@@ -80,53 +88,39 @@ void Otto::MoveServos(int time, int servo_target[]) {
SetRestState(false);
}
final_time_ = millis() + time;
if (time > 10) {
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1) {
increment_[i] = (servo_target[i] - servo_[i].GetPosition()) / (time / 10.0);
}
}
for (int iteration = 1; millis() < final_time_; iteration++) {
partial_time_ = millis() + 10;
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1) {
servo_[i].SetPosition(servo_[i].GetPosition() + increment_[i]);
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
} else {
if (time <= 10) {
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1) {
servo_[i].SetPosition(servo_target[i]);
}
}
vTaskDelay(pdMS_TO_TICKS(time));
return;
}
// final adjustment to the target.
bool f = true;
int adjustment_count = 0;
while (f && adjustment_count < 10) {
f = false;
int start[SERVO_COUNT];
for (int i = 0; i < SERVO_COUNT; i++) {
start[i] = servo_[i].GetPosition();
}
int steps = std::max(1, time / 10);
for (int step = 1; step <= steps; step++) {
float t = static_cast<float>(step) / static_cast<float>(steps);
float eased_t = EaseOutCubic(t);
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1 && servo_target[i] != servo_[i].GetPosition()) {
f = true;
break;
if (servo_pins_[i] != -1) {
float interpolated = start[i] + (servo_target[i] - start[i]) * eased_t;
servo_[i].SetPosition(static_cast<int>(std::round(interpolated)));
}
}
if (f) {
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1) {
servo_[i].SetPosition(servo_target[i]);
}
}
vTaskDelay(pdMS_TO_TICKS(10));
adjustment_count++;
vTaskDelay(pdMS_TO_TICKS(10));
}
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1) {
servo_[i].SetPosition(servo_target[i]);
}
};
}
}
void Otto::MoveSingle(int position, int servo_number) {
@@ -192,11 +186,18 @@ void Otto::Execute(int amplitude[SERVO_COUNT], int offset[SERVO_COUNT], int peri
///////////////////////////////////////////////////////////////////
void Otto::Home(bool hands_down) {
if (is_otto_resting_ == false) { // Go to rest position only if necessary
MoveServos(1000, servo_initial_);
int max_delta = 0;
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1) {
max_delta = std::max(max_delta, std::abs(servo_initial_[i] - servo_[i].GetPosition()));
}
}
int home_time = std::clamp(500 + max_delta * 9, 500, 1700);
MoveServos(home_time, servo_initial_);
is_otto_resting_ = true;
}
vTaskDelay(pdMS_TO_TICKS(1000));
vTaskDelay(pdMS_TO_TICKS(200));
}
bool Otto::GetRestState() {

View File

@@ -406,7 +406,12 @@ private:
}
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);
UBaseType_t pending_actions =
uxQueueMessagesWaiting(controller->action_queue_);
// 如果后面还有动作,先不归位,避免“动作末尾急停+马上再启动”
if (pending_actions == 0) {
controller->otto_.Home(params.action_type != ACTION_HANDS_UP);
}
}
}
}

View File

@@ -1,6 +1,7 @@
#include "otto_movements.h"
#include <algorithm>
#include <cmath>
#include "freertos/idf_additions.h"
#include "oscillator.h"
@@ -9,6 +10,13 @@ static const char* TAG = "OttoMovements";
#define HAND_HOME_POSITION 45
namespace {
float EaseOutCubic(float t) {
float inv = 1.0f - t;
return 1.0f - inv * inv * inv;
}
} // namespace
Otto::Otto() {
is_otto_resting_ = false;
has_hands_ = false;
@@ -92,53 +100,40 @@ void Otto::MoveServos(int time, int servo_target[]) {
SetRestState(false);
}
final_time_ = millis() + time;
if (time > 10) {
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1) {
increment_[i] = (servo_target[i] - servo_[i].GetPosition()) / (time / 10.0);
}
}
for (int iteration = 1; millis() < final_time_; iteration++) {
partial_time_ = millis() + 10;
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1) {
servo_[i].SetPosition(servo_[i].GetPosition() + increment_[i]);
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
} else {
if (time <= 10) {
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1) {
servo_[i].SetPosition(servo_target[i]);
}
}
vTaskDelay(pdMS_TO_TICKS(time));
return;
}
// final adjustment to the target.
bool f = true;
int adjustment_count = 0;
while (f && adjustment_count < 10) {
f = false;
int start[SERVO_COUNT];
for (int i = 0; i < SERVO_COUNT; i++) {
start[i] = servo_[i].GetPosition();
}
int steps = std::max(1, time / 10);
for (int step = 1; step <= steps; step++) {
float t = static_cast<float>(step) / static_cast<float>(steps);
float eased_t = EaseOutCubic(t);
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1 && servo_target[i] != servo_[i].GetPosition()) {
f = true;
break;
if (servo_pins_[i] != -1) {
float interpolated =
start[i] + (servo_target[i] - start[i]) * eased_t;
servo_[i].SetPosition(static_cast<int>(std::round(interpolated)));
}
}
if (f) {
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1) {
servo_[i].SetPosition(servo_target[i]);
}
}
vTaskDelay(pdMS_TO_TICKS(10));
adjustment_count++;
vTaskDelay(pdMS_TO_TICKS(10));
}
for (int i = 0; i < SERVO_COUNT; i++) {
if (servo_pins_[i] != -1) {
servo_[i].SetPosition(servo_target[i]);
}
};
}
}
void Otto::MoveSingle(int position, int servo_number) {
@@ -239,6 +234,7 @@ void Otto::Home(bool hands_down) {
if (is_otto_resting_ == false) { // Go to rest position only if necessary
// 为所有舵机准备初始位置值
int homes[SERVO_COUNT];
int max_delta = 0;
for (int i = 0; i < SERVO_COUNT; i++) {
if (i == LEFT_HAND || i == RIGHT_HAND) {
if (hands_down) {
@@ -256,9 +252,15 @@ void Otto::Home(bool hands_down) {
// 腿部和脚部舵机始终复位
homes[i] = 90;
}
if (servo_pins_[i] != -1) {
max_delta = std::max(max_delta, std::abs(homes[i] - servo_[i].GetPosition()));
}
}
MoveServos(700, homes);
// 位移越大,归位时间越长,避免末端“急刹车”感
int home_time = std::clamp(500 + max_delta * 9, 500, 1700);
MoveServos(home_time, homes);
is_otto_resting_ = true;
}