cmvr-es-cli/biohead_test.py

344 lines
12 KiB
Python
Raw Normal View History

2025-10-21 09:33:47 +08:00
#!/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()