168 lines
5.6 KiB
Python
168 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
text_dgceshi.py — 写死 5 组表情(已按 XML 限位约束),每 5 秒切换,循环发送到 ESP32
|
||
依赖:pip install pyserial
|
||
用法:
|
||
python text_dgceshi.py /dev/ttyUSB0 [baudrate]
|
||
|
||
帧格式(与你现有固件一致):
|
||
[0] 0xAA
|
||
[1] count = 指令条数(每条是5字节:addr, ch, ang, durL, durH)
|
||
[2..] 指令序列,共 count*5 字节
|
||
[end] XOR 校验(对前面所有字节异或)
|
||
"""
|
||
|
||
import sys
|
||
import time
|
||
import serial
|
||
|
||
FRAME_HEADER = 0xAA
|
||
|
||
def calc_checksum(data: bytearray) -> int:
|
||
"""XOR 校验"""
|
||
cs = 0
|
||
for b in data:
|
||
cs ^= b
|
||
return cs & 0xFF
|
||
|
||
def pack_pose(raw_list):
|
||
"""
|
||
raw_list: [addr, ch, ang, durL, durH, addr, ch, ang, durL, durH, ...]
|
||
返回带帧头/数量/校验的完整数据包 bytes
|
||
"""
|
||
assert len(raw_list) % 5 == 0, "原始列表长度必须是 5 的倍数"
|
||
count = len(raw_list) // 5
|
||
pkt = bytearray([FRAME_HEADER, count & 0xFF])
|
||
pkt.extend(int(x) & 0xFF for x in raw_list)
|
||
pkt.append(calc_checksum(pkt))
|
||
return bytes(pkt)
|
||
|
||
def print_packet(pkt):
|
||
print("发送数据包:", ' '.join(f"0x{b:02X}" for b in pkt))
|
||
|
||
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.02)
|
||
ser.setDTR(True)
|
||
ser.setRTS(True)
|
||
ser.write(b"\r\n")
|
||
time.sleep(0.2)
|
||
except Exception as e:
|
||
print(f"[WARN] 唤醒失败:{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)
|
||
|
||
# =======================
|
||
# 五组“写死”的表情数据(严格遵守你的 XML 安全限位)
|
||
# 64:0~3 眉(offset 90/90/90/90,限位 45~120/…/50~120)
|
||
# 64:4~9 眼(offset 90..,限位 [90~170, 10~90, 10~90, 90~170, 10~170, 10~170])
|
||
# 65:0~9 嘴(offset 90..,限位 [60~130, 60~130, 50~120, 50~130, 50~100, 50~130, 50~130, 90~120, 91~120, 60~89])
|
||
# duration 全部为 0(立即执行);如需时长,可把毫秒拆成 durL/durH 小端写入
|
||
# =======================
|
||
|
||
POSES_RAW = [
|
||
# # Pose 1 — 中性位置(全部 offset)
|
||
[
|
||
# 板 64 — 眼眉 0~3
|
||
64,0,90,0,0, 64,1,90,0,0, 64,2,90,0,0, 64,3,90,0,0,
|
||
# 板 64 — 眼睛 4~9(选在各自安全范围内的中值/视觉舒适值)
|
||
64,4,160,0,0, 64,5,20,0,0, 64,6,20,0,0, 64,7,160,0,0, 64,8,150,0,0, 64,9,150,0,0,
|
||
# 板 65 — 嘴巴 0~9
|
||
65,0,90,0,0, 65,1,90,0,0, 65,2,85,0,0, 65,3,90,0,0, 65,4,90,0,0,
|
||
65,5,90,0,0, 65,6,90,0,0, 65,7,90,0,0, 65,8,90,0,0, 65,9,90,0,0
|
||
],
|
||
|
||
# Pose 5 — 大笑(接近上限但不越界)
|
||
[
|
||
64,0,115,0,0, 64,1,75,0,0, 64,2,75,0,0, 64,3,115,0,0,
|
||
64,4,160,0,0, 64,5,20,0,0, 64,6,20,0,0, 64,7,160,0,0, 64,8,50,0,0, 64,9,40,0,0,
|
||
65,0,50,0,0, 65,1,110,0,0, 65,2,70,0,0, 65,3,50,0,0, 65,4,100,0,0,
|
||
65,5,110,0,0, 65,6,130,0,0, 65,7,90,0,0, 65,8,110,0,0, 65,9,70,0,0
|
||
],
|
||
|
||
|
||
[
|
||
64,0,115,0,0, 64,1,75,0,0, 64,2,75,0,0, 64,3,115,0,0,
|
||
64,4,160,0,0, 64,5,20,0,0, 64,6,20,0,0, 64,7,160,0,0, 64,8,50,0,0, 64,9,130,0,0,
|
||
65,0,50,0,0, 65,1,110,0,0, 65,2,70,0,0, 65,3,50,0,0, 65,4,90,0,0,
|
||
65,5,110,0,0, 65,6,130,0,0, 65,7,90,0,0, 65,8,90,0,0, 65,9,90,0,0
|
||
],
|
||
[
|
||
64,0,115,0,0, 64,1,75,0,0, 64,2,75,0,0, 64,3,115,0,0,
|
||
64,4,160,0,0, 64,5,20,0,0, 64,6,20,0,0, 64,7,160,0,0, 64,8,50,0,0, 64,9,40,0,0,
|
||
65,0,50,0,0, 65,1,110,0,0, 65,2,70,0,0, 65,3,50,0,0, 65,4,90,0,0,
|
||
65,5,110,0,0, 65,6,130,0,0, 65,7,90,0,0, 65,8,110,0,0, 65,9,70,0,0
|
||
]
|
||
]
|
||
|
||
POSES_RAW1 = [[
|
||
# 板 64 — 眼眉 0~3
|
||
64,0,90,0,0, 64,1,90,0,0, 64,2,90,0,0, 64,3,90,0,0,
|
||
# 板 64 — 眼睛 4~9(选在各自安全范围内的中值/视觉舒适值)
|
||
64,4,90,0,0, 64,5,90,0,0, 64,6,90,0,0, 64,7,90,0,0, 64,8,150,0,0, 64,9,150,0,0,
|
||
# 板 65 — 嘴巴 0~9
|
||
65,0,90,0,0, 65,1,90,0,0, 65,2,85,0,0, 65,3,90,0,0, 65,4,90,0,0,
|
||
65,5,90,0,0, 65,6,90,0,0, 65,7,90,0,0, 65,8,90,0,0, 65,9,90,0,0
|
||
],
|
||
|
||
# Pose 5 — 大笑(接近上限但不越界)
|
||
[
|
||
64,0,115,0,0, 64,1,75,0,0, 64,2,75,0,0, 64,3,115,0,0,
|
||
64,4,160,0,0, 64,5,20,0,0, 64,6,20,0,0, 64,7,160,0,0, 64,8,50,0,0, 64,9,40,0,0,
|
||
65,0,50,0,0, 65,1,110,0,0, 65,2,70,0,0, 65,3,50,0,0, 65,4,100,0,0,
|
||
65,5,110,0,0, 65,6,130,0,0, 65,7,90,0,0, 65,8,110,0,0, 65,9,70,0,0
|
||
],
|
||
|
||
|
||
]
|
||
|
||
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
|
||
|
||
# 可选:尝试唤醒 ESP32
|
||
wakeup_esp32(port, baud)
|
||
|
||
ser = open_serial(port, baud)
|
||
|
||
try:
|
||
pose_idx = 0
|
||
hold_seconds = 5
|
||
print(f"开始循环播放 5 组表情,每组间隔 {hold_seconds} 秒...")
|
||
while True:
|
||
pose = POSES_RAW[pose_idx]
|
||
pkt = pack_pose(pose)
|
||
print(f"\n→ 发送 Pose {pose_idx+1}/{len(POSES_RAW)}")
|
||
print_packet(pkt)
|
||
ser.write(pkt)
|
||
ser.flush()
|
||
time.sleep(hold_seconds)
|
||
pose_idx = (pose_idx + 1) % len(POSES_RAW)
|
||
except KeyboardInterrupt:
|
||
print("\n用户中断,退出")
|
||
finally:
|
||
ser.close()
|
||
print("串口已关闭")
|
||
|
||
if __name__ == "__main__":
|
||
main() |