新增语音控制机器人各部位舵机功能
1. RP2040 新增一次性动作系统(oneshot): - 支持多次来回摆动,动作完成后自动恢复原位 - 动作期间临时提升舵机速度(boost 8-15倍),确保全幅度快速响应 - 动作执行期间暂停常规状态动画和人脸追踪,避免舵机目标冲突 - 支持12个单动作(左右耳/左右眼球/点头/摇头/歪头/张嘴/眨眼/闭眼/睁眼/转身) - 支持6个组合动作(双耳/点头眨眼/摇头/张望/卖萌/跳舞) 2. ESP32 新增语音指令识别(stt关键词匹配): - 从用户语音识别结果中匹配中文关键词,直接发送动作指令给RP2040 - 不依赖AI回复格式,用户说话后立即触发动作 - 支持ASR识别变体容错(如"耳朵"/"耳刀"/"耳"均可匹配) 3. ESP32 新增AI回复文本动作解析(备用通道): - 支持从AI回复文本中解析全角括号包裹的动作代码 4. 新增LLM情绪标签日志,便于调试AI返回内容 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c63b35a0e7
commit
2a77110654
@ -319,6 +319,207 @@ state_map={
|
|||||||
"state_calibrate": state_calibrate
|
"state_calibrate": state_calibrate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#_________________# 一次性动作系统 #_________________#
|
||||||
|
|
||||||
|
oneshot_active = False # 是否正在执行一次性动作
|
||||||
|
oneshot_start_time = 0
|
||||||
|
oneshot_duration = 0 # 每段(半程)持续时间
|
||||||
|
oneshot_phase = 0 # 当前阶段:0,1,2,...,total_phases-1
|
||||||
|
oneshot_total_phases = 0 # 总阶段数(来回次数×2 + 1恢复阶段)
|
||||||
|
oneshot_pos_a = {} # A 位置(摆动极限一侧)
|
||||||
|
oneshot_pos_b = {} # B 位置(摆动极限另一侧)
|
||||||
|
saved_positions = {} # 动作前各舵机位置快照
|
||||||
|
|
||||||
|
saved_speeds = {} # 动作前各舵机的速度/加速度备份
|
||||||
|
|
||||||
|
def save_servo_positions():
|
||||||
|
"""保存当前所有舵机实际位置和运动参数"""
|
||||||
|
global saved_positions, saved_speeds
|
||||||
|
saved_positions = {}
|
||||||
|
saved_speeds = {}
|
||||||
|
for name, servo in servos.items():
|
||||||
|
saved_positions[name] = servo.pos
|
||||||
|
saved_speeds[name] = (servo.max_speed, servo.max_accel)
|
||||||
|
|
||||||
|
def restore_servo_positions():
|
||||||
|
"""恢复舵机到保存的位置和运动参数(只恢复被动作修改过的舵机)"""
|
||||||
|
for name in oneshot_pos_a:
|
||||||
|
if name in saved_positions and name in servos:
|
||||||
|
servos[name].set_target(saved_positions[name])
|
||||||
|
if name in saved_speeds and name in servos:
|
||||||
|
servos[name].max_speed, servos[name].max_accel = saved_speeds[name]
|
||||||
|
servos[name]._inv_2_accel = 1.0 / (2.0 * servos[name].max_accel)
|
||||||
|
|
||||||
|
def boost_servos(names):
|
||||||
|
"""临时提升指定舵机的速度和加速度,让动作又快又猛"""
|
||||||
|
for name in names:
|
||||||
|
if name in servos:
|
||||||
|
servos[name].max_speed = 2000.0 # 原来 250,提升 8 倍
|
||||||
|
servos[name].max_accel = 3000.0 # 原来 200,提升 15 倍
|
||||||
|
servos[name]._inv_2_accel = 1.0 / (2.0 * 3000.0)
|
||||||
|
|
||||||
|
def start_oneshot(pos_a, pos_b, duration_ms=200, repeats=3):
|
||||||
|
"""
|
||||||
|
启动多次来回摆动的一次性动作
|
||||||
|
pos_a: {舵机名: 角度} — 摆动一侧极限
|
||||||
|
pos_b: {舵机名: 角度} — 摆动另一侧极限(None 则用保存的原位)
|
||||||
|
duration_ms: 每半程持续时间(越小越快)
|
||||||
|
repeats: 来回摆动次数
|
||||||
|
"""
|
||||||
|
global oneshot_active, oneshot_start_time, oneshot_duration
|
||||||
|
global oneshot_phase, oneshot_total_phases
|
||||||
|
global oneshot_pos_a, oneshot_pos_b
|
||||||
|
|
||||||
|
if oneshot_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
save_servo_positions()
|
||||||
|
oneshot_pos_a = pos_a
|
||||||
|
# pos_b 为 None 时用保存的原位作为 B 位置
|
||||||
|
if pos_b is None:
|
||||||
|
oneshot_pos_b = {}
|
||||||
|
for name in pos_a:
|
||||||
|
if name in saved_positions:
|
||||||
|
oneshot_pos_b[name] = saved_positions[name]
|
||||||
|
else:
|
||||||
|
oneshot_pos_b = pos_b
|
||||||
|
oneshot_duration = duration_ms
|
||||||
|
oneshot_phase = 0
|
||||||
|
oneshot_total_phases = repeats * 2 + 1
|
||||||
|
oneshot_start_time = time.ticks_ms()
|
||||||
|
oneshot_active = True
|
||||||
|
|
||||||
|
# 临时提升涉及的舵机速度,让动作又快又猛
|
||||||
|
boost_servos(pos_a.keys())
|
||||||
|
if pos_b:
|
||||||
|
boost_servos(pos_b.keys())
|
||||||
|
|
||||||
|
# 第一阶段:去 A 位置
|
||||||
|
for name, angle in pos_a.items():
|
||||||
|
if name in servos:
|
||||||
|
servos[name].set_target(angle)
|
||||||
|
|
||||||
|
def update_oneshot():
|
||||||
|
"""主循环中调用,管理多次来回摆动"""
|
||||||
|
global oneshot_active, oneshot_phase, oneshot_start_time
|
||||||
|
|
||||||
|
if not oneshot_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
elapsed = time.ticks_diff(time.ticks_ms(), oneshot_start_time)
|
||||||
|
if elapsed < oneshot_duration:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 当前阶段完成,进入下一阶段
|
||||||
|
oneshot_phase += 1
|
||||||
|
oneshot_start_time = time.ticks_ms()
|
||||||
|
|
||||||
|
if oneshot_phase >= oneshot_total_phases:
|
||||||
|
# 所有阶段完成,恢复原位
|
||||||
|
restore_servo_positions()
|
||||||
|
oneshot_active = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# 奇数阶段去 B,偶数阶段去 A
|
||||||
|
side = "B" if (oneshot_phase % 2 == 1) else "A"
|
||||||
|
targets = oneshot_pos_b if (oneshot_phase % 2 == 1) else oneshot_pos_a
|
||||||
|
for name, angle in targets.items():
|
||||||
|
if name in servos:
|
||||||
|
servos[name].set_target(angle)
|
||||||
|
|
||||||
|
# --- 动作定义 ---
|
||||||
|
# duration_ms=每半程时间(越小越快),repeats=来回次数
|
||||||
|
|
||||||
|
def act_ear_left():
|
||||||
|
# 左耳:全幅度快速来回 3 次(速度已 boost,250ms 足够跑满 90° 行程)
|
||||||
|
start_oneshot({"EAL": 60}, {"EAL": 150}, 250, 3)
|
||||||
|
|
||||||
|
def act_ear_right():
|
||||||
|
start_oneshot({"EAR": 120}, {"EAR": 30}, 250, 3)
|
||||||
|
|
||||||
|
def act_eye_left():
|
||||||
|
start_oneshot({"EYL": 30, "EYR": 30}, {"EYL": 150, "EYR": 150}, 250, 3)
|
||||||
|
|
||||||
|
def act_eye_right():
|
||||||
|
start_oneshot({"EYL": 150, "EYR": 150}, {"EYL": 30, "EYR": 30}, 250, 3)
|
||||||
|
|
||||||
|
def act_tilt_left():
|
||||||
|
start_oneshot({"ROL": 30}, {"ROL": 120}, 300, 2)
|
||||||
|
|
||||||
|
def act_tilt_right():
|
||||||
|
start_oneshot({"ROL": 120}, {"ROL": 30}, 300, 2)
|
||||||
|
|
||||||
|
def act_nod():
|
||||||
|
# 大幅度快速点头 3 次
|
||||||
|
start_oneshot({"PIT": 1}, {"PIT": 80}, 250, 3)
|
||||||
|
|
||||||
|
def act_mouth():
|
||||||
|
start_oneshot({"MOU": 5}, {"MOU": 150}, 200, 3)
|
||||||
|
|
||||||
|
def act_blink():
|
||||||
|
# 快速眨眼 3 次
|
||||||
|
start_oneshot({"LID": 30}, {"LID": 110}, 150, 3)
|
||||||
|
|
||||||
|
def act_eyes_close():
|
||||||
|
global oneshot_active
|
||||||
|
if oneshot_active:
|
||||||
|
return
|
||||||
|
save_servo_positions()
|
||||||
|
servos["LID"]._write_pwm(30)
|
||||||
|
|
||||||
|
def act_eyes_open():
|
||||||
|
global oneshot_active
|
||||||
|
if oneshot_active:
|
||||||
|
return
|
||||||
|
save_servo_positions()
|
||||||
|
servos["LID"]._write_pwm(110)
|
||||||
|
|
||||||
|
def act_body_turn():
|
||||||
|
start_oneshot({"YAW": 30}, {"YAW": 150}, 300, 2)
|
||||||
|
|
||||||
|
# --- 组合动作 ---
|
||||||
|
|
||||||
|
def act_ears_both():
|
||||||
|
start_oneshot({"EAL": 60, "EAR": 120}, {"EAL": 150, "EAR": 30}, 250, 3)
|
||||||
|
|
||||||
|
def act_nod_blink():
|
||||||
|
start_oneshot({"PIT": 1, "LID": 30}, {"PIT": 80, "LID": 110}, 250, 3)
|
||||||
|
|
||||||
|
def act_shake_head():
|
||||||
|
start_oneshot({"ROL": 30}, {"ROL": 120}, 250, 3)
|
||||||
|
|
||||||
|
def act_look_around():
|
||||||
|
start_oneshot({"EYL": 30, "EYR": 30, "YAW": 50}, {"EYL": 150, "EYR": 150, "YAW": 130}, 300, 2)
|
||||||
|
|
||||||
|
def act_cute():
|
||||||
|
start_oneshot({"ROL": 50, "LID": 30, "EAL": 60}, {"ROL": 120, "LID": 110, "EAL": 150}, 250, 2)
|
||||||
|
|
||||||
|
def act_dance():
|
||||||
|
start_oneshot({"YAW": 40, "ROL": 50, "EAL": 60, "EAR": 120}, {"YAW": 140, "ROL": 120, "EAL": 150, "EAR": 30}, 300, 3)
|
||||||
|
|
||||||
|
action_map = {
|
||||||
|
# 单动作
|
||||||
|
"act_ear_left": act_ear_left,
|
||||||
|
"act_ear_right": act_ear_right,
|
||||||
|
"act_eye_left": act_eye_left,
|
||||||
|
"act_eye_right": act_eye_right,
|
||||||
|
"act_tilt_left": act_tilt_left,
|
||||||
|
"act_tilt_right": act_tilt_right,
|
||||||
|
"act_nod": act_nod,
|
||||||
|
"act_mouth": act_mouth,
|
||||||
|
"act_blink": act_blink,
|
||||||
|
"act_eyes_close": act_eyes_close,
|
||||||
|
"act_eyes_open": act_eyes_open,
|
||||||
|
"act_body_turn": act_body_turn,
|
||||||
|
# 组合动作
|
||||||
|
"act_ears_both": act_ears_both,
|
||||||
|
"act_nod_blink": act_nod_blink,
|
||||||
|
"act_shake_head": act_shake_head,
|
||||||
|
"act_look_around": act_look_around,
|
||||||
|
"act_cute": act_cute,
|
||||||
|
"act_dance": act_dance,
|
||||||
|
}
|
||||||
|
|
||||||
#_________________# #_________________#
|
#_________________# #_________________#
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -123,7 +123,10 @@ while True:
|
|||||||
# Grab all pending commands from the ESP
|
# Grab all pending commands from the ESP
|
||||||
incoming_commands = external.esp_read()
|
incoming_commands = external.esp_read()
|
||||||
for data in incoming_commands:
|
for data in incoming_commands:
|
||||||
if data in animation.state_map:
|
if data in animation.action_map:
|
||||||
|
# 一次性动作:不改变 current_state
|
||||||
|
animation.action_map[data]()
|
||||||
|
elif data in animation.state_map:
|
||||||
animation.new_state_flag = True
|
animation.new_state_flag = True
|
||||||
animation.current_state = data
|
animation.current_state = data
|
||||||
|
|
||||||
@ -137,8 +140,8 @@ while True:
|
|||||||
# Snap the mouth closed when speech ends!
|
# Snap the mouth closed when speech ends!
|
||||||
animation.servos["MOU"].set_target(130)
|
animation.servos["MOU"].set_target(130)
|
||||||
|
|
||||||
# 3. Speaking timer / animation
|
# 3. Speaking timer / animation(一次性动作期间暂停,避免覆盖 MOU 目标)
|
||||||
if speaking_flag:
|
if speaking_flag and not animation.oneshot_active:
|
||||||
# Set the mouth position based on the current toggle state
|
# Set the mouth position based on the current toggle state
|
||||||
if bool_a == False:
|
if bool_a == False:
|
||||||
animation.servos["MOU"].set_target(130)
|
animation.servos["MOU"].set_target(130)
|
||||||
@ -157,6 +160,9 @@ while True:
|
|||||||
blink_counter = blink_time
|
blink_counter = blink_time
|
||||||
|
|
||||||
|
|
||||||
|
# 更新一次性动作状态
|
||||||
|
animation.update_oneshot()
|
||||||
|
|
||||||
# Calibration mode
|
# Calibration mode
|
||||||
if (mode.value() == 1):
|
if (mode.value() == 1):
|
||||||
animation.current_state = "state_calibrate"
|
animation.current_state = "state_calibrate"
|
||||||
@ -165,7 +171,8 @@ while True:
|
|||||||
if LED_countdown <= 0:
|
if LED_countdown <= 0:
|
||||||
LED.toggle()
|
LED.toggle()
|
||||||
LED_countdown = LED_oscillator
|
LED_countdown = LED_oscillator
|
||||||
else:
|
elif not animation.oneshot_active:
|
||||||
|
# 一次性动作期间跳过常规状态动画,避免舵机目标冲突
|
||||||
if animation.current_state == "idle":
|
if animation.current_state == "idle":
|
||||||
animation.apply_state("idle")
|
animation.apply_state("idle")
|
||||||
animation.servos["LID"]._write_pwm(30)
|
animation.servos["LID"]._write_pwm(30)
|
||||||
@ -193,7 +200,12 @@ while True:
|
|||||||
animation.servos["LID"]._write_pwm(110)
|
animation.servos["LID"]._write_pwm(110)
|
||||||
animation.current_state = "neutral"
|
animation.current_state = "neutral"
|
||||||
|
|
||||||
facetrack()
|
# 一次性动作期间暂停人脸追踪,避免覆盖 EYL/EYR/PIT/YAW 目标
|
||||||
|
# 但仍需读取 Grove 数据防止串口缓冲区溢出
|
||||||
|
if animation.oneshot_active:
|
||||||
|
external.grove_read() # 消费数据,不驱动舵机
|
||||||
|
else:
|
||||||
|
facetrack()
|
||||||
|
|
||||||
# Update all servos except eyelids
|
# Update all servos except eyelids
|
||||||
for name, s in servos.items():
|
for name, s in servos.items():
|
||||||
|
|||||||
@ -475,6 +475,57 @@ void Application::Start() {
|
|||||||
auto text = cJSON_GetObjectItem(root, "text");
|
auto text = cJSON_GetObjectItem(root, "text");
|
||||||
if (cJSON_IsString(text)) {
|
if (cJSON_IsString(text)) {
|
||||||
ESP_LOGI(TAG, "<< %s", text->valuestring);
|
ESP_LOGI(TAG, "<< %s", text->valuestring);
|
||||||
|
// 从 AI 回复文本中解析动作代码,发送给 RP2040
|
||||||
|
// 格式:(EAL)好啦好啦~ TTS 的 IgnoreBracketText 会自动跳过()内容
|
||||||
|
// (= UTF-8: 0xEF 0xBC 0x88 )= UTF-8: 0xEF 0xBC 0x89
|
||||||
|
const char* t = text->valuestring;
|
||||||
|
if ((unsigned char)t[0] == 0xEF && (unsigned char)t[1] == 0xBC && (unsigned char)t[2] == 0x88) {
|
||||||
|
// 找到全角左括号(,搜索对应的)
|
||||||
|
const char* content = t + 3;
|
||||||
|
const char* end = content;
|
||||||
|
while (*end) {
|
||||||
|
if ((unsigned char)end[0] == 0xEF && (unsigned char)end[1] == 0xBC && (unsigned char)end[2] == 0x89) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
if (*end) {
|
||||||
|
int code_len = end - content;
|
||||||
|
if (code_len > 0 && code_len <= 8) {
|
||||||
|
char action_code[9] = {};
|
||||||
|
memcpy(action_code, content, code_len);
|
||||||
|
static const struct { const char* code; const char* cmd; } action_table[] = {
|
||||||
|
// 单动作
|
||||||
|
{"EAL", "act_ear_left"},
|
||||||
|
{"EAR", "act_ear_right"},
|
||||||
|
{"EYL", "act_eye_left"},
|
||||||
|
{"EYR", "act_eye_right"},
|
||||||
|
{"TLL", "act_tilt_left"},
|
||||||
|
{"TLR", "act_tilt_right"},
|
||||||
|
{"NOD", "act_nod"},
|
||||||
|
{"MOU", "act_mouth"},
|
||||||
|
{"BLK", "act_blink"},
|
||||||
|
{"ECL", "act_eyes_close"},
|
||||||
|
{"EOP", "act_eyes_open"},
|
||||||
|
{"BTN", "act_body_turn"},
|
||||||
|
// 组合动作
|
||||||
|
{"EARB", "act_ears_both"},
|
||||||
|
{"NDBK", "act_nod_blink"},
|
||||||
|
{"SHAK", "act_shake_head"},
|
||||||
|
{"LOOK", "act_look_around"},
|
||||||
|
{"CUTE", "act_cute"},
|
||||||
|
{"DANC", "act_dance"},
|
||||||
|
};
|
||||||
|
for (const auto& entry : action_table) {
|
||||||
|
if (strcmp(action_code, entry.code) == 0) {
|
||||||
|
ESP_LOGI(TAG, "Action: %s -> %s", action_code, entry.cmd);
|
||||||
|
uart_send_string(entry.cmd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Schedule([this, display, message = std::string(text->valuestring)]() {
|
Schedule([this, display, message = std::string(text->valuestring)]() {
|
||||||
display->SetChatMessage("assistant", message.c_str());
|
display->SetChatMessage("assistant", message.c_str());
|
||||||
});
|
});
|
||||||
@ -484,6 +535,58 @@ void Application::Start() {
|
|||||||
auto text = cJSON_GetObjectItem(root, "text");
|
auto text = cJSON_GetObjectItem(root, "text");
|
||||||
if (cJSON_IsString(text)) {
|
if (cJSON_IsString(text)) {
|
||||||
ESP_LOGI(TAG, ">> %s", text->valuestring);
|
ESP_LOGI(TAG, ">> %s", text->valuestring);
|
||||||
|
// 从用户语音指令中匹配身体动作关键词,直接发送给 RP2040
|
||||||
|
const char* s = text->valuestring;
|
||||||
|
const char* action_cmd = nullptr;
|
||||||
|
// 判断是否包含"耳"相关("耳朵"或"耳",ASR 可能识别为"耳刀"等)
|
||||||
|
bool has_ear = strstr(s, "耳") != nullptr;
|
||||||
|
bool has_left = strstr(s, "左") != nullptr;
|
||||||
|
bool has_right = strstr(s, "右") != nullptr;
|
||||||
|
// 优先匹配带方向的耳朵
|
||||||
|
if (has_ear && has_left) {
|
||||||
|
action_cmd = "act_ear_left";
|
||||||
|
} else if (has_ear && has_right) {
|
||||||
|
action_cmd = "act_ear_right";
|
||||||
|
} else if (has_ear) {
|
||||||
|
action_cmd = "act_ears_both";
|
||||||
|
// 头部动作
|
||||||
|
} else if (strstr(s, "点头") || strstr(s, "点一下头") || strstr(s, "点点头")) {
|
||||||
|
action_cmd = "act_nod";
|
||||||
|
} else if (strstr(s, "摇头") || strstr(s, "摇摇头") || strstr(s, "摇一下头")) {
|
||||||
|
action_cmd = "act_shake_head";
|
||||||
|
} else if (strstr(s, "歪") && has_left) {
|
||||||
|
action_cmd = "act_tilt_left";
|
||||||
|
} else if (strstr(s, "歪") && has_right) {
|
||||||
|
action_cmd = "act_tilt_right";
|
||||||
|
} else if (strstr(s, "歪头") || strstr(s, "歪一下")) {
|
||||||
|
action_cmd = "act_tilt_left";
|
||||||
|
// 眼睛动作
|
||||||
|
} else if ((strstr(s, "眼") || strstr(s, "看")) && has_left) {
|
||||||
|
action_cmd = "act_eye_left";
|
||||||
|
} else if ((strstr(s, "眼") || strstr(s, "看")) && has_right) {
|
||||||
|
action_cmd = "act_eye_right";
|
||||||
|
} else if (strstr(s, "眨眼") || strstr(s, "眨一下")) {
|
||||||
|
action_cmd = "act_blink";
|
||||||
|
} else if (strstr(s, "闭眼") || strstr(s, "闭上眼")) {
|
||||||
|
action_cmd = "act_eyes_close";
|
||||||
|
} else if (strstr(s, "睁眼") || strstr(s, "睁开眼")) {
|
||||||
|
action_cmd = "act_eyes_open";
|
||||||
|
// 嘴巴
|
||||||
|
} else if (strstr(s, "张嘴") || strstr(s, "嘴巴") || strstr(s, "张开嘴")) {
|
||||||
|
action_cmd = "act_mouth";
|
||||||
|
// 身体
|
||||||
|
} else if (strstr(s, "转身") || strstr(s, "转动") || strstr(s, "转个") || strstr(s, "转一下")) {
|
||||||
|
action_cmd = "act_body_turn";
|
||||||
|
// 组合/特殊
|
||||||
|
} else if (strstr(s, "卖萌")) {
|
||||||
|
action_cmd = "act_cute";
|
||||||
|
} else if (strstr(s, "跳舞") || strstr(s, "跳个舞")) {
|
||||||
|
action_cmd = "act_dance";
|
||||||
|
}
|
||||||
|
if (action_cmd) {
|
||||||
|
ESP_LOGI(TAG, "Voice action: [%s] -> %s", s, action_cmd);
|
||||||
|
uart_send_string(action_cmd);
|
||||||
|
}
|
||||||
Schedule([this, display, message = std::string(text->valuestring)]() {
|
Schedule([this, display, message = std::string(text->valuestring)]() {
|
||||||
display->SetChatMessage("user", message.c_str());
|
display->SetChatMessage("user", message.c_str());
|
||||||
});
|
});
|
||||||
@ -492,6 +595,7 @@ void Application::Start() {
|
|||||||
// LLM 回复中携带的情绪标签(如 "happy"、"thinking")
|
// LLM 回复中携带的情绪标签(如 "happy"、"thinking")
|
||||||
auto emotion = cJSON_GetObjectItem(root, "emotion");
|
auto emotion = cJSON_GetObjectItem(root, "emotion");
|
||||||
if (cJSON_IsString(emotion)) {
|
if (cJSON_IsString(emotion)) {
|
||||||
|
ESP_LOGI(TAG, "LLM emotion: [%s]", emotion->valuestring);
|
||||||
Schedule([this, display, emotion_str = std::string(emotion->valuestring)]() {
|
Schedule([this, display, emotion_str = std::string(emotion->valuestring)]() {
|
||||||
display->SetEmotion(emotion_str.c_str());
|
display->SetEmotion(emotion_str.c_str());
|
||||||
// 将 AI 返回的情绪标签发送给 RP2040,实时切换舵机动画表情
|
// 将 AI 返回的情绪标签发送给 RP2040,实时切换舵机动画表情
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user