cmvr-es-cli/biohead_test.py
2025-10-21 09:33:47 +08:00

344 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
interactive_servo_test.py — 基于 Python 的 ESP32 舵机控制交互脚本
依赖pip install pyserial
用法:
python3 interactive_servo_test.py /dev/ttyUSB0 [baudrate]
功能:
1. 控制单个舵机
2. 控制多个舵机
3. 退出
4. 自动增减模式:从 0 到 180° 每步 +6° → -6° 循环50Hz 频率发送
5. 设置自动模式舵机数量(通道 1~N
6. 眨眼模式让五个舵机执行预设角度循环模拟眨眼8号舵机保持120度
7. 打哈欠模式:模拟打哈欠动作
8. 说话模式:模拟嘴巴开合动作
"""
import sys
import time
import serial
import random
FRAME_HEADER = 0xAA # 帧头
def wakeup_esp32(port, baud=115200):
"""通过 RTS/DTR 线复位并唤醒 ESP32"""
try:
ser = serial.Serial(port, baud, timeout=1)
ser.setDTR(False)
ser.setRTS(False)
time.sleep(0.01)
ser.setDTR(True)
ser.setRTS(True)
ser.write(b"\r\n")
time.sleep(1)
except Exception as e:
print(f"ESP32 唤醒失败: {e}")
finally:
try:
ser.close()
except:
pass
def open_serial(port, baud):
"""打开并配置串口"""
try:
ser = serial.Serial(port, baud, timeout=1)
print(f"已打开串口: {port} @ {baud}bps")
return ser
except Exception as e:
print(f"打开串口失败: {e}")
sys.exit(1)
def calc_checksum(data: bytearray) -> int:
"""计算 XOR 校验和"""
cs = 0
for b in data:
cs ^= b
return cs
def pack_commands(cmds):
"""
cmds: list of (addr, channel, angle, duration_ms)
返回:完整帧字节数组
格式: [FRAME_HEADER][count] [addr, ch, ang, durL, durH]... [checksum]
"""
pkt = bytearray([FRAME_HEADER, len(cmds)])
for addr, ch, ang, dur in cmds:
pkt.extend([addr, ch, ang, dur & 0xFF, (dur >> 8) & 0xFF])
pkt.append(calc_checksum(pkt))
return pkt
def print_packet(pkt):
"""打印十六进制数据包"""
print("发送数据包:", ' '.join(f"0x{b:02X}" for b in pkt))
def input_hex(prompt):
"""输入 0x 开头或十进制整型"""
val = input(prompt).strip()
try:
return int(val, 0)
except ValueError:
print(f"无效数字: {val}")
return input_hex(prompt)
def input_cmd_single():
"""交互输入单条命令"""
addr = input_hex("输入 addr (hex or dec): ")
ch = input_hex("输入 channel (hex or dec): ")
ang = input_hex("输入 angle (0-180): ")
dur = input_hex("输入 duration(ms): ")
return [(addr, ch, ang, dur)]
def input_cmd_multiple():
"""交互输入多条命令"""
cnt = input_hex("输入命令数量: ")
cmds = []
for i in range(cnt):
print(f"{i+1} 条:")
cmds.extend(input_cmd_single())
return cmds
def auto_mode(ser, channels):
"""自动增减模式0→180→0步长650Hz多舵机"""
addr = input_hex("自动模式: 输入 addr (hex或dec): ")
interval = 1.0 / 50.0
dur_ms = int(interval * 1000)
angle = 0
direction = 1
step = 6
print(f"启动自动模式 @50Hz, addr=0x{addr:02X}, channels={channels}, step={step}°")
try:
while True:
cmds = [(addr, ch, angle, dur_ms) for ch in channels]
pkt = pack_commands(cmds)
print_packet(pkt)
ser.write(pkt)
# 更新角度
angle += direction * step
if angle >= 180:
angle = 180
direction = -1
elif angle <= 0:
angle = 0
direction = 1
time.sleep(interval)
except KeyboardInterrupt:
print("\n自动模式已停止,返回菜单")
def set_channels():
"""自定义舵机数量,返回通道列表 1~N"""
cnt = input_hex("输入自动模式舵机数量 N: ")
if cnt < 1:
print("数量必须 >= 1")
return None
channels = list(range(1, cnt+1))
print(f"已设置通道列表: {channels}")
return channels
def blink_mode(ser):
"""眨眼模式:根据输入的角度进行指定舵机的眨眼操作"""
addr = input_hex("眨眼模式: 输入 addr (hex或dec): ")
blink_count = 10 # 循环10次
interval = 0.1 # 每秒眨眼一次
# 定义五个舵机的通道
servo_channels = [4, 5, 6, 7, 8] # 控制五个舵机
blink_angles = [90, 90, 90, 90, 90] # 眨眼时的角度
open_angles = [50, 130, 130, 60, 90] # 睁开时的角度4-7号舵机回到90度8号保持120度
print(f"启动眨眼模式10次眨眼每次间隔 0.1 秒")
try:
for i in range(blink_count):
print(f"{i+1} 次眨眼...")
# 眨眼动作:同步发送所有舵机到眨眼角度
cmds = [(addr, ch, angle, 0) for ch, angle in zip(servo_channels, blink_angles)]
pkt = pack_commands(cmds)
print_packet(pkt)
ser.write(pkt)
time.sleep(interval)
# 睁开动作同步发送4-7号舵机回到90度8号保持120度
print("睁开动作舵机4-7回到90度")
cmds = [(addr, ch, angle, 0) for ch, angle in zip(servo_channels, open_angles)]
pkt = pack_commands(cmds)
print_packet(pkt)
ser.write(pkt)
time.sleep(interval)
print("眨眼模式结束,返回菜单")
except KeyboardInterrupt:
print("\n眨眼模式已停止,返回菜单")
def yawn_mode(ser):
"""打哈欠模式:控制多个舵机模拟打哈欠动作"""
addr = input_hex("打哈欠模式: 输入 addr (hex或dec): ")
yawn_count = 5 # 循环5次
interval = 1 # 每次打哈欠动作间隔1秒
# 定义控制的舵机通道
# 打哈欠时使用舵机1, 3, 4, 5, 6, 7, 8通道1~8
# 闭嘴时使用舵机8、965号舵机
yawning_channels = [1, 3, 4, 5, 6, 7, 8] # 控制通道1~8
yawning_angles = [70, 110, 60, 130, 120, 80, 90] # 打哈欠时的角度(根据需要调整)
closing_angles = [90, 90, 90, 90, 90, 90, 90] # 闭嘴时的角度(闭嘴动作)
# 65号舵机8、9号通道设置不同的角度模拟控制头部动作
yawning_65_angles = [120, 45, 60, 120] # 65号舵机8、9号通道在打哈欠时的角度
closing_65_angles = [120, 45, 90, 90] # 闭嘴时65号舵机8、9号通道的角度
print(f"启动打哈欠模式,循环 {yawn_count} 次,每次间隔 {interval}")
try:
for i in range(yawn_count):
print(f"{i+1} 次打哈欠...")
# 打哈欠动作:同步发送所有舵机到打哈欠角度
cmds = [(addr, ch, angle, 3000) for ch, angle in zip(yawning_channels, yawning_angles)]
cmds_65 = [(65, ch, angle, 3000) for ch, angle in zip([0,1,8, 9], yawning_65_angles)] # 控制65号舵机8、9号通道
cmds.extend(cmds_65) # 合并65号舵机的指令
pkt = pack_commands(cmds)
print_packet(pkt)
ser.write(pkt) # 一次性发送所有指令
time.sleep(interval)
# 闭嘴动作:同步发送舵机回到闭嘴角度
print("闭嘴动作:所有舵机回到闭嘴角度")
cmds = [(addr, ch, angle, 3000) for ch, angle in zip(yawning_channels, closing_angles)]
cmds_65 = [(65, ch, angle, 3000) for ch, angle in zip([0,1,8, 9], closing_65_angles)] # 65号舵机8、9号通道
cmds.extend(cmds_65) # 合并65号舵机的指令
pkt = pack_commands(cmds)
print_packet(pkt)
ser.write(pkt) # 一次性发送所有指令
time.sleep(interval)
print("打哈欠模式结束,返回菜单")
except KeyboardInterrupt:
print("\n打哈欠模式已停止,返回菜单")
def talk_mode(ser):
"""说话模式:模拟嘴巴开合动作"""
addr = input_hex("说话模式: 输入 addr (hex或dec): ")
talk_count = 50 # 循环5次
# 定义控制的舵机通道
mouth_channels = [0, 1, 8, 9] # 控制3个舵机口型
mouth_open_angles = [120, 45, 80, 110] # 嘴巴打开时的角度
mouth_close_angles = [120, 45, 90, 90] # 嘴巴关闭时的角度(正常闭合)
print(f"启动说话模式,循环 {talk_count}每次间隔随机生成在0~1秒之间")
try:
for i in range(talk_count):
print(f"{i + 1} 次说话...")
# 开口动作:同步发送所有舵机到嘴巴打开的角度
cmds = [(addr, ch, angle, 0) for ch, angle in zip(mouth_channels, mouth_open_angles)]
pkt = pack_commands(cmds)
print_packet(pkt)
ser.write(pkt) # 一次性发送所有指令
# 生成一个 0 到 1 之间的随机间隔时间
interval = random.uniform(0, 0.3)
print(f"随机间隔时间:{interval:.2f}")
time.sleep(interval)
# 闭口动作:同步发送所有舵机回到嘴巴关闭的角度
print("闭口动作:舵机回到闭口角度")
cmds = [(addr, ch, angle, 0) for ch, angle in zip(mouth_channels, mouth_close_angles)]
pkt = pack_commands(cmds)
print_packet(pkt)
ser.write(pkt) # 一次性发送所有指令
# 生成一个 0 到 1 之间的随机间隔时间
interval = random.uniform(0, 0.3)
print(f"随机间隔时间:{interval:.2f}")
time.sleep(interval)
print("说话模式结束,返回菜单")
except KeyboardInterrupt:
print("\n说话模式已停止,返回菜单")
def main():
if len(sys.argv) < 2:
print(f"用法: {sys.argv[0]} /dev/ttyUSB0 [baudrate]")
sys.exit(1)
port = sys.argv[1]
baud = int(sys.argv[2]) if len(sys.argv) > 2 else 115200
print("唤醒 ESP32...")
wakeup_esp32(port, baud)
ser = open_serial(port, baud)
channels = list(range(1, 10)) # 默认通道 1-9
try:
while True:
print("\n=== 操作菜单 ===")
print("1. 控制单个舵机")
print("2. 控制多个舵机")
print("3. 退出")
print("4. 自动增减模式(50Hz)")
print("5. 设置自动模式舵机数量")
print("6. 眨眼模式")
print("7. 打哈欠模式")
print("8. 说话模式") # 新增选项
opt = input("选择: ").strip()
if opt == '1':
cmds = input_cmd_single()
elif opt == '2':
cmds = input_cmd_multiple()
elif opt == '4':
auto_mode(ser, channels)
continue
elif opt == '5':
new_ch = set_channels()
if new_ch:
channels = new_ch
continue
elif opt == '6':
blink_mode(ser)
continue
elif opt == '7': # 调用打哈欠模式
yawn_mode(ser)
continue
elif opt == '8': # 调用说话模式
talk_mode(ser)
continue
elif opt == '3':
break
else:
print("无效选项,重试")
continue
pkt = pack_commands(cmds)
print_packet(pkt)
ser.write(pkt)
print("✅ 指令已发送")
except KeyboardInterrupt:
print("\n用户中断,退出")
finally:
ser.close()
print("串口已关闭")
if __name__ == '__main__':
main()