199 lines
5.4 KiB
Python
199 lines
5.4 KiB
Python
#!/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)
|
||
"""
|
||
import sys
|
||
import time
|
||
import serial
|
||
|
||
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,步长6,50Hz,多舵机"""
|
||
addr = input_hex("自动模式: 输入 addr (hex or 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 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. 设置自动模式舵机数量")
|
||
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 == '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()
|