初始化

This commit is contained in:
lhx
2026-06-09 10:19:38 +08:00
commit d03691451f
6 changed files with 1589 additions and 0 deletions

Binary file not shown.

362
ir_bruteforce.py Normal file
View File

@@ -0,0 +1,362 @@
# -*- coding: utf-8 -*-
"""
红外板 — 发送协议穷举测试
========================
从接收已知: 客户码=3BC4, 方向键=0D/10/15/12
逐个尝试常见红外板发送格式,找出正确的那一个。
用法:
python ir_bruteforce.py -p COM8 # 自动试探所有协议
python ir_bruteforce.py -p COM8 -k 0D # 指定键值测试
python ir_bruteforce.py -p COM8 --list # 列出所有候选协议
每次发送后观察投影仪是否有反应,按 y/n 确认,脚本记录有效协议。
"""
import sys
import time
import argparse
import itertools
import serial
import serial.tools.list_ports
BAUDRATE = 9600
# 已知参数
CUSTOMER_CODE = bytes([0x3B, 0xC4]) # 客户码
KEY_CODES = {
"up": 0x0D, # 上
"left": 0x10, # 左
"down": 0x15, # 下
"right": 0x12, # 右
}
def build_frame(proto_name: str, key: int) -> bytes | None:
"""
根据协议名构建发送帧。
常见红外板发送协议:
- raw3: 直接 3 字节 [客户码H, 客户码L, 键值]
- nec_std: NEC 标准帧 [地址, ~地址, 命令, ~命令]
- nec_3B: 地址 0x3B (单字节) 的 NEC [0x3B, 0xC4, key, ~key]
- a5_prefix: A5 前缀协议 [A5, len, 数据..., checksum]
- aa_prefix: AA 前缀协议 [AA, 数据..., sum]
- text_crlf: 文本协议 "SEND xxxxxx\\r\\n"
- text_no_cr: 文本协议 "3BC40D"
- at_cmd: AT 指令 "AT+IR=xxxxxx\\r\\n"
- nec_full: NEC 完整帧 [addrL, addrH, cmd, ~cmd] (带反码)
"""
hi, lo = CUSTOMER_CODE[0], CUSTOMER_CODE[1]
# ── 原始字节类 ──
if proto_name == "raw3":
return bytes([hi, lo, key])
if proto_name == "raw3_reversed":
return bytes([key, lo, hi])
if proto_name == "raw3_with_cr":
return bytes([hi, lo, key, 0x0D])
if proto_name == "raw3_with_crlf":
return bytes([hi, lo, key, 0x0D, 0x0A])
# ── NEC 标准变体 ──
if proto_name == "nec_3bytes":
# 最简 NEC: 地址 + 命令 (不带反码)
return bytes([hi, lo, key])
if proto_name == "nec_addr_inv":
# NEC: [地址, ~地址, 命令, ~命令]
return bytes([hi, hi ^ 0xFF, key, key ^ 0xFF])
if proto_name == "nec_addr_inv_both":
# NEC 有时用两字节地址: [addrL, addrH, cmd, ~cmd]
return bytes([lo, hi, key, key ^ 0xFF])
if proto_name == "nec_no_invert":
# 只发地址+命令,不做反码
return bytes([lo, key])
if proto_name == "nec_lo_cmd":
# [0x3B, key] — 只有客户码低字节+键值
return bytes([lo, key])
if proto_name == "nec_hi_cmd":
# [0x3B, 0xC4, key] — raw3 即此格式
return bytes([hi, key])
# ── 带前缀/帧头 ──
if proto_name == "a5_f5":
# 常见: [A5, len, data..., sum]
data = bytes([hi, lo, key])
frame = bytes([0xA5, len(data)]) + data
csum = sum(frame) & 0xFF
return frame + bytes([csum])
if proto_name == "a5_f5_nosum":
data = bytes([hi, lo, key])
return bytes([0xA5, len(data)]) + data
if proto_name == "aa_55":
data = bytes([hi, lo, key])
csum = sum(data) & 0xFF
return bytes([0xAA, 0x55]) + data + bytes([csum])
if proto_name == "aa_55_nosum":
return bytes([0xAA, 0x55, hi, lo, key])
if proto_name == "ff_prefix":
return bytes([0xFF, 0xFF, hi, lo, key])
if proto_name == "fe_prefix":
return bytes([0xFE, hi, lo, key, 0xFD])
if proto_name == "a0_prefix":
return bytes([0xA0, hi, lo, key, 0x0A])
# ── 文本协议类 ──
hex_str = f"{hi:02X}{lo:02X}{key:02X}"
if proto_name == "text_hex":
return hex_str.encode("ascii")
if proto_name == "text_hex_cr":
return (hex_str + "\r").encode("ascii")
if proto_name == "text_hex_crlf":
return (hex_str + "\r\n").encode("ascii")
if proto_name == "text_hex_nl":
return (hex_str + "\n").encode("ascii")
if proto_name == "text_send":
return f"SEND {hex_str}".encode("ascii")
if proto_name == "text_send_crlf":
return f"IR_SEND={hex_str}\r\n".encode("ascii")
if proto_name == "text_at_crlf":
return f"AT+IR={hex_str}\r\n".encode("ascii")
if proto_name == "text_at_send_crlf":
return f"AT+SEND={hex_str}\r\n".encode("ascii")
# ── 带空格分隔的十六进制文本 ──
if proto_name == "text_spaced_crlf":
return f"{hi:02X} {lo:02X} {key:02X}\r\n".encode("ascii")
if proto_name == "text_spaced_nl":
return f"{hi:02X} {lo:02X} {key:02X}\n".encode("ascii")
# ── 先发命令再发数据 ──
if proto_name == "cmd_tx_data":
# 先发 TX 命令,延时 50ms再发数据
return None # 特殊处理
if proto_name == "cmd_0x01_prefix":
return bytes([0x01, hi, lo, key])
if proto_name == "cmd_0x02_prefix":
return bytes([0x02, hi, lo, key])
if proto_name == "cmd_0x03_prefix":
return bytes([0x03, hi, lo, key])
return None
# ═══════════════════════════════════════════════════════════════
# 所有候选协议 (按可能性排序)
# ═══════════════════════════════════════════════════════════════
PROTOCOLS = [
# ── 最可能的: 二进制直发 ──
("raw3", "二进制3字节 [3B C4 XX]"),
("raw3_reversed", "二进制3字节反转 [XX C4 3B]"),
("raw3_with_cr", "二进制3字节 + CR [3B C4 XX 0D]"),
("raw3_with_crlf", "二进制3字节 + CRLF [3B C4 XX 0D 0A]"),
# ── NEC 变体 ──
("nec_3bytes", "NEC简化 3字节 [3B C4 XX]"),
("nec_addr_inv", "NEC标准 [addr, ~addr, cmd, ~cmd]"),
("nec_addr_inv_both", "NEC双字节地址 [C4 3B XX ~XX]"),
("nec_lo_cmd", "NEC短帧 [C4 XX]"),
("nec_hi_cmd", "NEC短帧 [3B XX]"),
("nec_no_invert", "NEC无反码 [C4 XX]"),
# ── 带帧头 ──
("a5_f5", "A5帧头+校验 [A5 03 3B C4 XX SUM]"),
("a5_f5_nosum", "A5帧头无校验 [A5 03 3B C4 XX]"),
("aa_55", "AA55帧头 [AA 55 3B C4 XX SUM]"),
("aa_55_nosum", "AA55帧头无校验 [AA 55 3B C4 XX]"),
("ff_prefix", "FF前缀 [FF FF 3B C4 XX]"),
("fe_prefix", "FE帧 [FE 3B C4 XX FD]"),
("a0_prefix", "A0前缀 [A0 3B C4 XX 0A]"),
("cmd_0x01_prefix", "01前缀 [01 3B C4 XX]"),
("cmd_0x02_prefix", "02前缀 [02 3B C4 XX]"),
("cmd_0x03_prefix", "03前缀 [03 3B C4 XX]"),
# ── 文本协议 ──
("text_hex", "文本HEX \"3BC40D\""),
("text_hex_cr", "文本HEX+CR"),
("text_hex_crlf", "文本HEX+CRLF"),
("text_hex_nl", "文本HEX+LF"),
("text_spaced_crlf", "文本空格HEX \"3B C4 0D\\r\\n\""),
("text_spaced_nl", "文本空格HEX \"3B C4 0D\\n\""),
("text_send", "文本 \"SEND 3BC40D\""),
("text_send_crlf", "文本 \"IR_SEND=3BC40D\\r\\n\""),
("text_at_crlf", "AT指令 \"AT+IR=3BC40D\\r\\n\""),
("text_at_send_crlf", "AT指令 \"AT+SEND=3BC40D\\r\\n\""),
]
def scan_usb_ttl():
"""自动找 USB-to-TTL 端口"""
ports = serial.tools.list_ports.comports()
keywords = ["ch343", "ch340", "ch341", "usb-enhanced-serial",
"usb serial", "ft232", "cp210", "pl2303", "wch"]
for p in ports:
combined = f"{p.description or ''} {p.hwid or ''}".lower()
if any(kw in combined for kw in keywords):
return p.device
all_p = list(ports)
return all_p[0].device if all_p else None
def brute_force(port: str, key_hex: str = "0D", start_from: int = 0):
"""
逐个尝试所有协议。
每次发送后等待用户确认 (y=有效/n=无效/q=退出)
"""
key = int(key_hex, 16)
hi, lo = CUSTOMER_CODE[0], CUSTOMER_CODE[1]
hex_3b = f"{hi:02X}{lo:02X}{key:02X}"
print(f"串口: {port} @ {BAUDRATE}bps")
print(f"客户码: {hi:02X} {lo:02X}")
print(f"测试键值: {key:02X} ({hex_3b})")
print(f"候选协议: {len(PROTOCOLS)}\n")
ser = serial.Serial(port, baudrate=BAUDRATE, timeout=0.3)
print("=" * 65)
print(" 发送后观察投影仪是否响应,输入 y/n/q")
print("=" * 65)
working = []
total = len(PROTOCOLS)
skipped = 0
for i, (name, desc) in enumerate(PROTOCOLS):
if i < start_from:
skipped += 1
continue
frame = build_frame(name, key)
# 特殊协议: 先发送 "TX" 命令再发数据
if name == "cmd_tx_data":
try:
ser.write(b"TX\r\n")
ser.flush()
time.sleep(0.05)
ser.write(bytes([hi, lo, key]))
ser.flush()
except Exception as e:
print(f" [{i+1}/{total}] {name}: 发送失败 - {e}")
continue
print(f"\n[{i+1}/{total}] {name}")
print(f" 描述: {desc}")
print(f" 发送: TX\\r\\n → [3B C4 {key:02X}]")
elif frame is not None:
try:
ser.reset_output_buffer()
ser.write(frame)
ser.flush()
except Exception as e:
print(f" [{i+1}/{total}] {name}: 发送失败 - {e}")
continue
print(f"\n[{i+1}/{total}] {name}")
print(f" 描述: {desc}")
print(f" HEX: {frame.hex(' ').upper()}")
if all(32 <= b < 127 for b in frame):
print(f" ASCII: {frame.decode('ascii')}")
else:
continue
# 用户确认
while True:
resp = input(" 有效? [y/n/q/s(跳过)]: ").strip().lower()
if resp == 'y':
working.append((name, desc, frame.hex(" ").upper() if frame else "TX+data"))
print(f" ✓ 已记录")
break
elif resp == 'n':
break
elif resp == 'q':
print(f"\n已测试 {i+1-skipped}/{total}")
ser.close()
return working
elif resp == 's':
print(f" 跳过")
break
else:
print(" 请输入 y(有效) / n(无效) / q(退出)")
time.sleep(0.1)
ser.close()
print(f"\n测试完成: {i+1-skipped}/{total}")
return working
def main():
parser = argparse.ArgumentParser(
description="红外板发送协议 — 穷举测试",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("-p", "--port", default=None, help="串口 (如 COM8)")
parser.add_argument("-k", "--key", default="0D",
help=f"测试键值 hex (默认 0D=上). 已知: {', '.join(f'{k}={v:02X}' for k,v in KEY_CODES.items())}")
parser.add_argument("-s", "--start", type=int, default=0,
help="从第 N 个协议开始 (断点续测)")
parser.add_argument("--list", action="store_true", help="列出所有候选协议")
args = parser.parse_args()
if args.list:
print("候选发送协议:")
for i, (name, desc) in enumerate(PROTOCOLS):
print(f" [{i+1:>2}] {name:<22} {desc}")
return
port = args.port or scan_usb_ttl()
if not port:
print("✗ 未找到串口。用 -p 指定")
return
results = brute_force(port, args.key, start_from=args.start)
if results:
print("\n" + "=" * 65)
print(" 有效协议:")
for name, desc, frame_hex in results:
print(f"{name}: {desc}")
print(f" HEX: {frame_hex}")
else:
print("\n✗ 没有找到有效协议。")
print(" 可能原因:")
print(" 1. 波特率不是 9600 — 用 --auto-baud 再测")
print(" 2. 发送和接收需不同波特率")
print(' 3. 红外板需要先发送"进入发送模式"的指令')
print(" 4. 换个键值试试 -k 15")
if __name__ == "__main__":
main()

316
ir_diag.py Normal file
View File

@@ -0,0 +1,316 @@
# -*- coding: utf-8 -*-
"""
红外板 — 收发联合诊断
====================
一边发送候选协议帧,一边同时读取串口返回,
看板子收到数据后有没有任何回应ACK/NAK/回显等)。
同时抓取原始遥控信号的完整原始字节(不合并,带时间戳),
检查是否有被 IDLE_TIMEOUT 截断的帧头/帧尾。
用法:
python ir_diag.py -p COM8 # 诊断模式1: 重新抓遥控信号
python ir_diag.py -p COM8 --echo # 诊断模式2: 发送+监听回应
python ir_diag.py -p COM8 --boot # 诊断模式3: 捕获上电启动信息
"""
import sys
import time
import argparse
import serial
import serial.tools.list_ports
BAUDRATE = 9600
# 已知数据
CUSTOMER_HI = 0x3B
CUSTOMER_LO = 0xC4
KEY_CODES = {0x0D: "", 0x15: "", 0x10: "", 0x12: ""}
def find_port():
"""自动找 USB-to-TTL"""
ports = serial.tools.list_ports.comports()
keywords = ["ch343", "ch340", "ch341", "usb-enhanced-serial",
"usb serial", "ft232", "cp210", "pl2303", "wch"]
for p in ports:
combined = f"{p.description or ''} {p.hwid or ''}".lower()
if any(kw in combined for kw in keywords):
return p.device
all_p = list(ports)
return all_p[0].device if all_p else None
# ═══════════════════════════════════════════════════════════════
# 模式1: 详细捕获原始遥控信号 (逐字节, 不合并)
# ═══════════════════════════════════════════════════════════════
def capture_remote_detail(port: str):
"""
逐字节捕获,每个字节都带时间戳。
可能发现: 帧头(如 AA 55)、长度字节、校验和等之前被忽略的字节。
"""
print(f"打开 {port} @ {BAUDRATE} baud")
print("请按遥控器按键... (Ctrl+C 退出)\n")
ser = serial.Serial(port, baudrate=BAUDRATE, timeout=0.1)
print(f"{'时间':<12} {'字节':<6} {'HEX':<8} {'BIN':<12} {'ASCII'}")
print("-" * 65)
last_time = time.time()
try:
while True:
try:
n = ser.in_waiting
except serial.SerialException:
break
if n > 0:
now = time.time()
gap = now - last_time
data = ser.read(n)
for i, b in enumerate(data):
ascii_ch = chr(b) if 32 <= b < 127 else "."
ts = time.strftime("%H:%M:%S") + f".{int((now % 1) * 1000):03d}"
if i == 0 and gap > 0.3:
# 新的红外信号 — 画分隔线
print(f"\n{'' * 65}")
print(f" 新信号 ↑ 距上次 {gap:.1f}s")
print(f"{ts:<12} [{i+1}/{n}] {b:02X} {b:08b} {ascii_ch}")
last_time = now
else:
time.sleep(0.01)
except KeyboardInterrupt:
pass
finally:
ser.close()
print("\n✓ 诊断完成。检查上面的字节序列:")
print(" 1. 每次按键是否只有 3 个字节?")
print(" 2. 前后有没有额外的帧头/帧尾字节?")
print(" 3. 字节间有没有明显的停顿(说明是分帧发送)?")
# ═══════════════════════════════════════════════════════════════
# 模式2: 发送并监听回应
# ═══════════════════════════════════════════════════════════════
# 候选发送协议 — 带"编号"以便快速定位有效协议
CANDIDATES = [
# (名称, 帧bytes)
("raw_3bytes", bytes([0x3B, 0xC4, 0x0D])),
("raw_3bytes_rev", bytes([0x0D, 0xC4, 0x3B])),
("raw_plus_CR", bytes([0x3B, 0xC4, 0x0D, 0x0D])),
("raw_plus_CRLF", bytes([0x3B, 0xC4, 0x0D, 0x0D, 0x0A])),
("NEC_std", bytes([0x3B, 0xC4, 0x0D, 0xF2])),
("NEC_addr_inv", bytes([0x3B, 0xC4 ^ 0xFF, 0x0D, 0x0D ^ 0xFF])),
("A5_frame", bytes([0xA5, 0x03, 0x3B, 0xC4, 0x0D])),
("A5_frame_sum", bytes([0xA5, 0x03, 0x3B, 0xC4, 0x0D, 0xE9])),
("AA55_frame", bytes([0xAA, 0x55, 0x3B, 0xC4, 0x0D])),
("AA55_frame_sum", bytes([0xAA, 0x55, 0x3B, 0xC4, 0x0D, 0xA2])),
("FF_prefix", bytes([0xFF, 0xFF, 0x3B, 0xC4, 0x0D])),
("FE_FD_frame", bytes([0xFE, 0x3B, 0xC4, 0x0D, 0xFD])),
("A0_prefix", bytes([0xA0, 0x3B, 0xC4, 0x0D, 0x0A])),
("01_prefix", bytes([0x01, 0x3B, 0xC4, 0x0D])),
("AT_send_crlf", b"AT+SEND=3BC40D\r\n"),
("text_hex_crlf", b"3BC40D\r\n"),
("text_spaced_crlf", b"3B C4 0D\r\n"),
("text_send_crlf", b"SEND 3BC40D\r\n"),
("text_at_crlf", b"AT+IR=3BC40D\r\n"),
("IR_SEND_crlf", b"IR_SEND=3BC40D\r\n"),
]
def send_and_listen(port: str):
"""
发送每个候选帧,然后立即监听串口回应。
使用非阻塞读取: 发完等100ms把期间收到的所有字节打印出来。
"""
print(f"打开 {port} @ {BAUDRATE} baud")
print("收发联合诊断 — 每个协议发送后监听板子是否有回应\n")
ser = serial.Serial(port, baudrate=BAUDRATE, timeout=0.1)
for i, (name, frame) in enumerate(CANDIDATES):
# 清空接收缓冲
ser.reset_input_buffer()
ser.reset_output_buffer()
# 发送
try:
ser.write(frame)
ser.flush()
except serial.SerialException as e:
print(f" [{i+1:>2}] {name}: ✗ 发送失败 — {e}")
continue
# 等待回应 (给板子足够的处理时间)
time.sleep(0.15)
# 读取所有可用数据
response = bytearray()
while True:
n = ser.in_waiting
if n == 0:
break
response.extend(ser.read(n))
time.sleep(0.02)
# 打印结果
resp_str = ""
if response:
resp_str = f"← 板子回应 {len(response)}B: {response.hex(' ').upper()}"
try:
ascii_str = response.decode("ascii", errors="replace").strip()
if ascii_str:
resp_str += f" [{ascii_str}]"
except Exception:
pass
print(f" [{i+1:>2}] {name}")
print(f" 发送: {frame.hex(' ').upper()}")
if resp_str:
print(f" {resp_str}")
else:
print(f" (板子无回应)")
time.sleep(0.05)
ser.close()
print("\n" + "=" * 65)
print("分析要点:")
print(" 1. 有回应的 → 可能是板子返回了 ACK/NAK/回显,说明协议接近")
print(" 2. 回应 = 发送内容 → 回显模式,说明板子期望特定格式")
print(" 3. 回应包含 'OK'/'ERROR' 等 → 文本协议")
print(" 4. 全无回应 → 要么协议全错,要么板子是单向的(不回显)")
print("\n提示: 同时也用手机摄像头看红外LED是否发光")
# ═══════════════════════════════════════════════════════════════
# 模式3: 上电启动捕获
# ═══════════════════════════════════════════════════════════════
def capture_boot(port: str):
"""
打开串口后立即开始接收,看板子有没有上电启动信息。
也尝试切换 DTR (部分板子用 DTR 供电/复位)。
"""
print("上电诊断 — 捕获板子启动信息\n")
print("提示: 拔掉 USB-to-TTL 再插入,然后按 Enter 继续...")
input()
# 先用 DTR 低电平"复位"
try:
ser = serial.Serial(port, baudrate=BAUDRATE, timeout=0.1)
ser.dtr = False
time.sleep(0.5)
ser.dtr = True
print(f"已打开 {port} (DTR 复位完成)")
except serial.SerialException as e:
print(f"✗ 无法打开 {port}: {e}")
return
print("等待板子启动输出... (10s)\n")
buffer = bytearray()
start = time.time()
try:
while time.time() - start < 10:
n = ser.in_waiting
if n > 0:
data = ser.read(n)
buffer.extend(data)
# 有数据就打印
ts = time.strftime("%H:%M:%S")
print(f" [{ts}] {data.hex(' ').upper()}", end="")
try:
ascii_str = data.decode("ascii", errors="replace").strip()
if ascii_str:
print(f" [{ascii_str}]")
else:
print()
except Exception:
print()
start = time.time() # 有新数据就重置计时
else:
time.sleep(0.05)
except KeyboardInterrupt:
pass
finally:
ser.close()
if buffer:
print(f"\n启动阶段共收到 {len(buffer)} 字节:")
print(f" HEX: {buffer.hex(' ').upper()}")
try:
text = buffer.decode("ascii", errors="replace")
if text.strip():
print(f" ASCII: {text.strip()}")
except Exception:
pass
else:
print("\n无启动输出 — 板子不发送启动信息")
# ═══════════════════════════════════════════════════════════════
# 综合诊断: 以上全部
# ═══════════════════════════════════════════════════════════════
def run_all(port: str):
"""运行全部诊断"""
print("=" * 65)
print(" 阶段1: 上电启动捕获")
print("=" * 65)
capture_boot(port)
print("\n" + "=" * 65)
print(" 阶段2: 详细遥控信号捕获")
print("=" * 65)
capture_remote_detail(port)
print("\n" + "=" * 65)
print(" 阶段3: 发送+监听回应")
print("=" * 65)
send_and_listen(port)
def main():
parser = argparse.ArgumentParser(
description="红外板 — 收发联合诊断",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("-p", "--port", default=None, help="串口")
parser.add_argument("--boot", action="store_true", help="捕获上电启动信息")
parser.add_argument("--echo", action="store_true", help="发送候选协议+监听回应")
parser.add_argument("--capture", action="store_true", help="详细捕获遥控信号")
parser.add_argument("--all", action="store_true", help="运行全部诊断")
args = parser.parse_args()
port = args.port or find_port()
if not port:
print("✗ 未找到串口。请用 -p 指定。")
return
print(f"串口: {port}")
if args.all:
run_all(port)
elif args.boot:
capture_boot(port)
elif args.echo:
send_and_listen(port)
elif args.capture:
capture_remote_detail(port)
else:
# 默认: 全部运行
run_all(port)
if __name__ == "__main__":
main()

290
ir_receive.py Normal file
View File

@@ -0,0 +1,290 @@
# -*- coding: utf-8 -*-
"""
红外收发板 — 接收测试
=====================
USB转TTL 连接红外收发板,接收并显示红外信号数据。
用法:
python ir_receive.py # 自动查找串口, 9600 波特率
python ir_receive.py --ports # 仅扫描串口
python ir_receive.py -p COM5 # 指定端口
python ir_receive.py -p COM5 -b 115200 # 指定端口和波特率
python ir_receive.py -p COM5 --hex-only # 仅显示十六进制
依赖: pip install pyserial
"""
import sys
import time
import argparse
import serial
import serial.tools.list_ports
# ═══════════════════════════════════════════════════════════════
# 常见红外板串口波特率 (按优先级排列)
# ═══════════════════════════════════════════════════════════════
COMMON_BAUDRATES = [9600, 115200, 57600, 19200, 38400, 4800, 2400]
# CH34x / 常见 USB-to-TTL 芯片 VID:PID
USB_TTL_CHIPS = {
(0x1A86, 0x55D3): "CH343",
(0x1A86, 0x55D4): "CH344",
(0x1A86, 0x7523): "CH340",
(0x1A86, 0x5523): "CH341",
(0x1A86, 0x55D5): "CH342",
(0x0403, 0x6001): "FT232",
(0x067B, 0x2303): "PL2303",
(0x10C4, 0xEA60): "CP2102",
}
USB_TTL_KEYWORDS = [
"ch343", "ch340", "ch341", "ch342", "ch344",
"usb-enhanced-serial", "usb serial", "usb-serial",
"ft232", "ftdi", "cp210", "pl2303", "wch",
]
def scan_ports():
"""扫描所有串口,标注 USB-to-TTL 设备"""
ports = serial.tools.list_ports.comports()
results = []
print("=" * 70)
print(f"{'端口':<8} {'芯片':<10} {'VID:PID':<12} {'描述'}")
print("-" * 70)
for port in ports:
vid_pid = (port.vid, port.pid) if port.vid and port.pid else None
chip = USB_TTL_CHIPS.get(vid_pid, "")
if not chip:
combined = f"{port.description or ''} {port.hwid or ''}".lower()
for kw in USB_TTL_KEYWORDS:
if kw in combined:
chip = "USB-TTL"
break
info = {
"device": port.device,
"description": port.description or "",
"vid": port.vid,
"pid": port.pid,
"chip": chip or None,
}
results.append(info)
vid_str = f"{port.vid:04X}:{port.pid:04X}" if vid_pid else "N/A"
chip_str = chip or "-"
print(f"{port.device:<8} {chip_str:<10} {vid_str:<12} {port.description or ''}")
print("=" * 70)
return results
def find_usb_ttl_port():
"""自动查找 USB-to-TTL 串口"""
ports = serial.tools.list_ports.comports()
candidates = []
for port in ports:
combined = f"{port.description or ''} {port.hwid or ''}".lower()
if any(kw in combined for kw in USB_TTL_KEYWORDS):
candidates.append(port)
if candidates:
p = candidates[0]
print(f"→ 自动选择: {p.device}{p.description}")
return p.device
# 回退: 返回第一个可用端口
all_ports = list(ports)
if all_ports:
p = all_ports[0]
print(f"→ 回退使用: {p.device}{p.description}")
return p.device
return None
def try_connect(port: str, baudrate: int, timeout: float = 1.0) -> serial.Serial | None:
"""尝试以指定波特率打开串口"""
try:
ser = serial.Serial(
port=port,
baudrate=baudrate,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=timeout,
)
return ser
except serial.SerialException:
return None
def format_hex(data: bytes) -> str:
"""格式化十六进制显示"""
return data.hex(" ").upper()
def format_binary(data: bytes) -> str:
"""格式化二进制显示"""
return " ".join(f"{b:08b}" for b in data)
def run_receiver(port: str, baudrate: int, hex_only: bool = False):
"""
主接收循环 — 持续读取串口数据并显示。
按 Ctrl+C 退出。
"""
print(f"\n打开串口 {port} @ {baudrate} baud...")
ser = try_connect(port, baudrate, timeout=0.5)
if ser is None:
print(f"✗ 无法打开 {port}")
return
print(f"✓ 已连接。等待红外信号... (按 Ctrl+C 退出)\n")
print("-" * 70)
buffer = bytearray()
last_print_time = time.time()
IDLE_PRINT_THRESHOLD = 0.15 # 空闲超过此秒数则打印之前累积的数据
try:
while True:
try:
waiting = ser.in_waiting
except (serial.SerialException, OSError) as e:
print(f"\n✗ 串口断开: {e}")
break
if waiting > 0:
try:
chunk = ser.read(waiting)
except serial.SerialException:
break
buffer.extend(chunk)
last_print_time = time.time()
else:
# 空闲时: 如果缓冲区有数据且空闲足够久,打印
if buffer and (time.time() - last_print_time) > IDLE_PRINT_THRESHOLD:
data = bytes(buffer)
timestamp = time.strftime("%H:%M:%S")
if hex_only:
print(f"[{timestamp}] ({len(data)}B) {format_hex(data)}")
else:
print(f"[{timestamp}] 收到 {len(data)} 字节")
print(f" HEX: {format_hex(data)}")
print(f" BIN: {format_binary(data)}")
# 尝试 ASCII 解码
try:
ascii_str = data.decode("ascii")
if ascii_str.isprintable():
print(f" ASCII: {ascii_str.strip()}")
except UnicodeDecodeError:
pass
print()
buffer.clear()
time.sleep(0.02)
except KeyboardInterrupt:
print("\n\n用户中断")
finally:
ser.close()
print(f"✓ 已关闭 {port}")
def auto_baudrate_test(port: str):
"""
波特率自动探测 — 提示用户按红外遥控器,
在不同波特率下监听,看哪个能收到有效数据。
"""
print("\n" + "=" * 70)
print(" 波特率自动探测")
print("=" * 70)
print("请持续按红外遥控器任意键...\n")
for baud in COMMON_BAUDRATES:
print(f" 测试 {baud:>6} baud ... ", end="", flush=True)
ser = try_connect(port, baud, timeout=0.3)
if ser is None:
print("无法打开")
continue
# 等待一小段时间收集数据
received = bytearray()
deadline = time.time() + 2.0
while time.time() < deadline:
try:
n = ser.in_waiting
if n > 0:
received.extend(ser.read(n))
except serial.SerialException:
break
time.sleep(0.05)
ser.close()
if received:
print(f"收到 {len(received)} 字节! → {format_hex(bytes(received))}")
else:
print("无数据")
print("\n提示: 有数据的波特率即为正确波特率。")
def main():
parser = argparse.ArgumentParser(
description="红外收发板 — 接收测试",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
%(prog)s 自动查找串口, 9600bps, 持续接收
%(prog)s --ports 仅扫描串口
%(prog)s -p COM5 指定端口接收
%(prog)s -p COM5 -b 115200 指定端口和波特率
%(prog)s -p COM5 --auto-baud 自动探测波特率
%(prog)s -p COM5 --hex-only 仅显示十六进制
""",
)
parser.add_argument("-p", "--port", default=None, help="串口 (如 COM5)")
parser.add_argument("-b", "--baudrate", type=int, default=9600, help="波特率 (默认 9600)")
parser.add_argument("--ports", action="store_true", help="仅扫描串口")
parser.add_argument("--auto-baud", action="store_true", help="自动探测波特率")
parser.add_argument("--hex-only", action="store_true", help="仅显示十六进制")
args = parser.parse_args()
# 仅扫描
if args.ports:
scan_ports()
return
# 确定端口
port = args.port
if port is None:
print("未指定端口,自动扫描 USB-to-TTL 设备...\n")
scan_ports()
port = find_usb_ttl_port()
if port is None:
print("\n✗ 未找到任何串口。请检查:")
print(" 1. USB-to-TTL 是否已插入")
print(" 2. 设备管理器中是否出现 COM 口")
print(" 3. 驱动是否已安装")
return
print()
# 自动探测波特率
if args.auto_baud:
auto_baudrate_test(port)
return
# 持续接收
run_receiver(port, args.baudrate, hex_only=args.hex_only)
if __name__ == "__main__":
main()

529
ir_send.py Normal file
View File

@@ -0,0 +1,529 @@
# -*- coding: utf-8 -*-
"""
红外收发板 — 发送脚本 (投影仪遥控)
===================================
通过 USB-TTL 连接红外收发板,发送红外信号控制投影仪。
协议说明 (来自板子文档):
发送红外信号: {A1, F1, <红外码3字节>}
修改通信地址: {A1, F2, <新地址>, 00, 00}
修改波特率: {A1, F3, <速率编号>, 00, 00}
其中:
A1 = 帧头
F1 = 发射红外指令
F2 = 修改通信地址
F3 = 修改波特率
用法:
python ir_send.py -p COM8 up # 上
python ir_send.py -p COM8 down # 下
python ir_send.py -p COM8 left # 左
python ir_send.py -p COM8 right # 右
python ir_send.py -p COM8 --raw 3BC40D # 发送原始红外码
python ir_send.py -p COM8 -i # 交互模式 (w/s/a/d 方向键控制)
依赖: pip install pyserial
"""
import sys
import time
import argparse
import serial
import serial.tools.list_ports
# ═══════════════════════════════════════════════════════════════
# 红外板协议常量
# ═══════════════════════════════════════════════════════════════
FRAME_HEADER = 0xA1 # 帧头
CMD_SEND_IR = 0xF1 # 发射红外信号
CMD_SET_ADDR = 0xF2 # 修改通信地址
CMD_SET_BAUD = 0xF3 # 修改波特率
DEFAULT_BAUDRATE = 9600
# ═══════════════════════════════════════════════════════════════
# 遥控器按键码 (从接收中采集到的红外原始码, 3字节)
# ═══════════════════════════════════════════════════════════════
IR_CODES = {
# 方向键
"up": bytes([0x3B, 0xC4, 0x0D]),
"left": bytes([0x3B, 0xC4, 0x10]),
"down": bytes([0x3B, 0xC4, 0x15]),
"right": bytes([0x3B, 0xC4, 0x12]),
# 继续通过接收测试添加...
# "ok": bytes([0x3B, 0xC4, 0x??]),
# "menu": bytes([0x3B, 0xC4, 0x??]),
# "power": bytes([0x3B, 0xC4, 0x??]),
}
# ═══════════════════════════════════════════════════════════════
# 帧构建
# ═══════════════════════════════════════════════════════════════
def build_send_frame(ir_code: bytes) -> bytes:
"""
构建红外发射帧: {A1, F1, <3字节红外码>}
参数:
ir_code: 3 字节红外原始码, 如 b'\x3B\xC4\x0D'
返回:
5 字节完整发送帧
"""
if len(ir_code) != 3:
raise ValueError(f"红外码必须是 3 字节, 实际 {len(ir_code)} 字节")
return bytes([FRAME_HEADER, CMD_SEND_IR]) + ir_code
def build_set_addr_frame(new_addr: int) -> bytes:
"""构建修改地址帧: {A1, F2, <新地址>, 00, 00}"""
return bytes([FRAME_HEADER, CMD_SET_ADDR, new_addr & 0xFF, 0x00, 0x00])
def build_set_baud_frame(baud_code: int) -> bytes:
"""
构建修改波特率帧: {A1, F3, <速率编号>, 00, 00}
已知波特率编号 (根据文档):
01 = 4800bps
(其他待补充, 默认 9600 可能对应 00 或其他值)
"""
return bytes([FRAME_HEADER, CMD_SET_BAUD, baud_code & 0xFF, 0x00, 0x00])
# ═══════════════════════════════════════════════════════════════
# 串口扫描
# ═══════════════════════════════════════════════════════════════
USB_TTL_KEYWORDS = [
"ch343", "ch340", "ch341", "ch342", "ch344",
"usb-enhanced-serial", "usb serial", "usb-serial",
"ft232", "ftdi", "cp210", "pl2303", "wch",
]
def find_port():
"""自动查找 USB-to-TTL 串口"""
ports = serial.tools.list_ports.comports()
for port in ports:
combined = f"{port.description or ''} {port.hwid or ''}".lower()
if any(kw in combined for kw in USB_TTL_KEYWORDS):
print(f"→ 自动选择: {port.device}{port.description}")
return port.device
all_ports = list(ports)
if all_ports:
print(f"→ 回退使用: {all_ports[0].device}")
return all_ports[0].device
return None
# ═══════════════════════════════════════════════════════════════
# 发送核心
# ═══════════════════════════════════════════════════════════════
def send_ir(ser: serial.Serial, ir_code: bytes, repeat: int = 1):
"""
参数:
ser: 已打开的串口
ir_code: 3字节红外原始码
repeat: 重复发送次数
"""
frame = build_send_frame(ir_code)
for i in range(repeat):
ser.write(frame)
ser.flush()
hex_str = ir_code.hex(" ").upper()
if repeat > 1:
print(f" 发送 [{i+1}/{repeat}]: {hex_str}")
else:
print(f" 发送: {hex_str} → 帧 {frame.hex(' ').upper()}")
if i < repeat - 1:
time.sleep(0.1)
def send_raw_ir(port: str, hex_str: str, repeat: int = 1):
"""发送原始 3 字节红外码 (hex 字符串)"""
hex_str = hex_str.replace(" ", "").replace("-", "")
if len(hex_str) != 6:
print(f"✗ 红外码应为 6 位 hex (3字节), 实际 {len(hex_str)}")
return
try:
ir_code = bytes.fromhex(hex_str)
except ValueError as e:
print(f"✗ HEX 格式错误: {e}")
return
ser = serial.Serial(port, baudrate=DEFAULT_BAUDRATE, timeout=0.5)
try:
send_ir(ser, ir_code, repeat)
finally:
ser.close()
def send_named(port: str, name: str, repeat: int = 1):
"""发送命名按键"""
name = name.lower()
ir_code = IR_CODES.get(name)
if ir_code is None:
print(f"✗ 未知按键: {name}")
print(f" 已知按键: {', '.join(IR_CODES.keys())}")
return
ser = serial.Serial(port, baudrate=DEFAULT_BAUDRATE, timeout=0.5)
try:
send_ir(ser, ir_code, repeat)
finally:
ser.close()
# ═══════════════════════════════════════════════════════════════
# 交互模式 (同时收发)
# ═══════════════════════════════════════════════════════════════
INTERACTIVE_HELP = """
╔══════════════════════════════════════════════╗
║ 投影仪红外遥控 — 收发一体交互模式 ║
╠══════════════════════════════════════════════╣
║ w/↑=上 s/↓=下 a/←=左 d/→=右 ║
║ Enter = 重复上次按键 ║
╠══════════════════════════════════════════════╣
║ :raw <hex> 发送自定义红外码 ║
║ :learn <名> <hex> 学习新按键 ║
║ :keys 列出已存按键 ║
║ :h 帮助 ║
║ :q 退出 ║
╠══════════════════════════════════════════════╣
║ 接收到的红外信号会实时显示在屏幕上 ║
╚══════════════════════════════════════════════╝
"""
KEY_MAP = {
'w': 'up', 's': 'down', 'a': 'left', 'd': 'right',
'up': 'up', 'down': 'down', 'left': 'left', 'right': 'right',
}
# 接收到的新信号 → 显示的标签
RX_LABELS = {
0x0D: "↑ 上",
0x15: "↓ 下",
0x10: "← 左",
0x12: "→ 右",
}
def _kbhit():
"""检查是否有按键 (非阻塞)"""
try:
import msvcrt
return msvcrt.kbhit()
except ImportError:
return False
def _getch():
"""读取单个按键 (阻塞, 仅在 kbhit() 返回 True 后调用)"""
try:
import msvcrt
ch = msvcrt.getch()
if ch == b'\xe0':
ch2 = msvcrt.getch()
return {
b'H': 'up', b'P': 'down', b'K': 'left', b'M': 'right',
}.get(ch2, '')
return ch.decode("utf-8", errors="replace").lower()
except ImportError:
return ""
def run_interactive(port: str, repeat: int = 1):
"""收发一体交互模式 — 同时接收红外信号 + 按键发送"""
import msvcrt
codes = dict(IR_CODES)
cfg = {"repeat": repeat} # 可变容器, 允许命令中修改
ser = serial.Serial(port, baudrate=DEFAULT_BAUDRATE, timeout=0)
print(f"✓ 已连接 {port} @ {DEFAULT_BAUDRATE} baud")
print(INTERACTIVE_HELP)
last_key = None # 上一个按键名 (用于 Enter 重复)
rx_buffer = bytearray()
rx_last_byte_time = 0
print("┌──────────────────────────────────────────┐")
print("│ 等待按键或红外信号... │")
print("└──────────────────────────────────────────┘")
try:
while True:
got_input = False
# ── 1. 检查串口接收 ──
try:
n = ser.in_waiting
except (serial.SerialException, OSError):
print("\n✗ 串口断开")
break
if n > 0:
data = ser.read(n)
now = time.time()
# 判断是否为新信号 (距上次字节 > 200ms)
if rx_buffer and (now - rx_last_byte_time) > 0.2:
# 打印上一个信号
_print_rx(bytes(rx_buffer))
rx_buffer.clear()
rx_buffer.extend(data)
rx_last_byte_time = now
got_input = True
# 缓冲区有数据但已空闲 → 打印
if rx_buffer and (time.time() - rx_last_byte_time) > 0.2:
_print_rx(bytes(rx_buffer))
rx_buffer.clear()
got_input = True
# ── 2. 检查键盘输入 (非阻塞) ──
if msvcrt.kbhit():
ch = _getch()
got_input = True
# 方向键
name = KEY_MAP.get(ch)
if name and name in codes:
send_ir(ser, codes[name], cfg["repeat"])
last_key = name
continue
# 回车 = 重复上次
if ch in ('\r', '\n'):
if last_key and last_key in codes:
send_ir(ser, codes[last_key], cfg["repeat"])
continue
# 命令模式
if ch in (':', '/', '\\'):
# 读取整行命令
print(f"\nIR> :", end="", flush=True)
cmd_line = _read_line()
_handle_command(ser, codes, cmd_line, cfg, last_key)
print("┌──────────────────────────────────────────┐")
print("│ 等待按键或红外信号... │")
print("└──────────────────────────────────────────┘")
continue
# 退出
if ch in ('q', '\x1b', 'esc'):
print("\n退出")
break
# 帮助
if ch in ('h', '?'):
print("\n" + INTERACTIVE_HELP)
continue
# ── 3. 空闲时短暂休眠 ──
if not got_input:
time.sleep(0.02)
except KeyboardInterrupt:
print("\n\n用户中断")
finally:
ser.close()
print(f"✓ 已关闭 {port}")
def _print_rx(data: bytes):
"""格式化打印接收到的红外信号"""
ts = time.strftime("%H:%M:%S")
hex_str = data.hex(" ").upper()
# 尝试识别
label = ""
if len(data) == 3 and data[0] == 0x3B and data[1] == 0xC4:
label = RX_LABELS.get(data[2], f"键值 0x{data[2]:02X}")
if label:
print(f"\n ← [{ts}] 收到: {hex_str} {label}")
else:
print(f"\n ← [{ts}] 收到: {hex_str} ({len(data)}字节)")
def _read_line() -> str:
"""读取一行输入 (在 kbhit 之后调用)"""
import msvcrt
line = ""
while True:
ch = msvcrt.getch()
if ch in (b'\r', b'\n'):
print()
return line
if ch == b'\x08': # Backspace
if line:
line = line[:-1]
print('\b \b', end='', flush=True)
continue
if ch == b'\x1b': # ESC 取消
print(" [取消]")
return ""
try:
c = ch.decode("utf-8")
line += c
print(c, end='', flush=True)
except UnicodeDecodeError:
pass
def _handle_command(ser, codes, line, cfg, last_key):
"""处理命令行输入"""
if not line:
return
parts = line.strip().split()
if not parts:
return
cmd = parts[0].lower()
if cmd in ('q', 'quit', 'exit'):
raise KeyboardInterrupt()
if cmd in ('h', 'help', '?'):
print(INTERACTIVE_HELP)
return
if cmd == 'keys':
print("已存按键:")
for k, v in codes.items():
marker = " ← 上次" if k == last_key else ""
print(f" {k:<12}{v.hex(' ').upper()}{marker}")
return
if cmd == 'learn' and len(parts) >= 3:
try:
data = bytes.fromhex(parts[2].replace(" ", ""))
codes[parts[1]] = data
print(f"{parts[1]}{data.hex(' ').upper()}")
except ValueError:
print(" ✗ HEX 格式错误")
return
if cmd == 'raw' and len(parts) >= 2:
hex_str = parts[1].replace(" ", "")
if len(hex_str) != 6:
print(f" ✗ 红外码应为 6 位 hex (3字节)")
return
try:
data = bytes.fromhex(hex_str)
send_ir(ser, data, cfg["repeat"])
except ValueError:
print(" ✗ HEX 格式错误")
return
if cmd == 'repeat' and len(parts) >= 2:
try:
cfg["repeat"] = int(parts[1])
print(f" ✓ 重复次数 = {cfg['repeat']}")
except ValueError:
print(" 用法: repeat <次数>")
return
# 命名按键
if cmd in codes:
send_ir(ser, codes[cmd], cfg["repeat"])
else:
print(f" ✗ 未知命令: {cmd}")
# ═══════════════════════════════════════════════════════════════
# 命令行入口
# ═══════════════════════════════════════════════════════════════
def main():
parser = argparse.ArgumentParser(
description="红外收发板 — 投影仪遥控 (A1 F1 协议)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
%(prog)s -p COM8 up 发送""
%(prog)s -p COM8 down 发送""
%(prog)s -p COM8 --raw 3BC40D 发送原始红外码
%(prog)s -p COM8 -i 交互模式 (w/s/a/d 方向键控制)
%(prog)s -p COM8 -i -n 3 交互模式, 每键重复3次
""",
)
parser.add_argument("key", nargs="?", default=None,
help="按键名 (up/down/left/right)")
parser.add_argument("-p", "--port", default=None, help="串口 (如 COM8)")
parser.add_argument("-b", "--baudrate", type=int, default=DEFAULT_BAUDRATE,
help=f"波特率 (默认 {DEFAULT_BAUDRATE})")
parser.add_argument("--raw", metavar="HEX", help="发送原始 3 字节红外码")
parser.add_argument("-n", "--repeat", type=int, default=1,
help="重复发送次数 (默认 1)")
parser.add_argument("-i", "--interactive", action="store_true",
help="交互模式 (w/s/a/d 方向键控制)")
parser.add_argument("--list", action="store_true", help="列出所有已知按键")
parser.add_argument("--set-addr", metavar="HEX", help="修改通信地址 (如 A5)")
parser.add_argument("--set-baud", metavar="CODE", help="修改波特率编号")
args = parser.parse_args()
# 确定端口
port = args.port or find_port()
if not port:
print("✗ 未找到串口。请用 -p 指定。")
return
# 列出按键
if args.list:
print("已知红外码:")
for key, code in IR_CODES.items():
print(f" {key:<12}{code.hex(' ').upper()}")
return
# 配置命令
if args.set_addr:
addr = int(args.set_addr, 16)
frame = build_set_addr_frame(addr)
ser = serial.Serial(port, baudrate=DEFAULT_BAUDRATE, timeout=0.5)
ser.write(frame)
ser.close()
print(f"✓ 已发送修改地址: {frame.hex(' ').upper()}")
return
if args.set_baud:
code = int(args.set_baud)
frame = build_set_baud_frame(code)
ser = serial.Serial(port, baudrate=DEFAULT_BAUDRATE, timeout=0.5)
ser.write(frame)
ser.close()
print(f"✓ 已发送修改波特率: {frame.hex(' ').upper()}")
return
# 交互模式
if args.interactive:
run_interactive(port, args.repeat)
return
# 单次发送
if args.raw:
send_raw_ir(port, args.raw, args.repeat)
elif args.key:
send_named(port, args.key, args.repeat)
else:
parser.print_help()
if __name__ == "__main__":
main()

92
ir_try.py Normal file
View File

@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
"""
快速尝试单个协议 — 带手机摄像头观察红外LED是否发光
"""
import sys
import time
import serial
PORT = sys.argv[1] if len(sys.argv) > 1 else "COM8"
KEY = 0x0D # 上
ser = serial.Serial(PORT, baudrate=9600, timeout=0.3)
# ═══════════════════════════════════════════════════════
# 尝试1: 完整4字节 NEC [addr, ~addr, cmd, ~cmd]
# ═══════════════════════════════════════════════════════
frame = bytes([0x3B, 0xC4, KEY, KEY ^ 0xFF])
print(f"尝试: NEC完整4字节 → {frame.hex(' ').upper()}")
print("用手机摄像头看红外LED是否发光...")
ser.reset_input_buffer()
ser.write(frame)
ser.flush()
time.sleep(0.15)
# 读回应
resp = ser.read(ser.in_waiting)
if resp:
print(f" 板子回应: {resp.hex(' ').upper()}")
else:
print(" (无回应)")
time.sleep(0.5)
# ═══════════════════════════════════════════════════════
# 尝试2: NEC 地址命令格式 [addr, cmd] (有些板子只要2字节)
# ═══════════════════════════════════════════════════════
frame = bytes([0x3B, KEY])
print(f"\n尝试: NEC短帧2字节 → {frame.hex(' ').upper()}")
print("用手机摄像头看红外LED是否发光...")
ser.reset_input_buffer()
ser.write(frame)
ser.flush()
time.sleep(0.15)
resp = ser.read(ser.in_waiting)
if resp:
print(f" 板子回应: {resp.hex(' ').upper()}")
else:
print(" (无回应)")
time.sleep(0.5)
# ═══════════════════════════════════════════════════════
# 尝试3: 带命令前缀的发送 (常见 YS-IRTM 风格)
# A1 F1 + 地址 + 命令
# ═══════════════════════════════════════════════════════
frame = bytes([0xA1, 0xF1, 0x3B, KEY])
print(f"\n尝试: A1F1命令格式 → {frame.hex(' ').upper()}")
print("用手机摄像头看红外LED是否发光...")
ser.reset_input_buffer()
ser.write(frame)
ser.flush()
time.sleep(0.15)
resp = ser.read(ser.in_waiting)
if resp:
print(f" 板子回应: {resp.hex(' ').upper()}")
else:
print(" (无回应)")
time.sleep(0.5)
# ═══════════════════════════════════════════════════════
# 尝试4: 换一种候选 — 单字节地址
# ═══════════════════════════════════════════════════════
frame = bytes([0xC4, KEY])
print(f"\n尝试: 单地址2字节 → {frame.hex(' ').upper()}")
ser.reset_input_buffer()
ser.write(frame)
ser.flush()
time.sleep(0.15)
resp = ser.read(ser.in_waiting)
if resp:
print(f" 板子回应: {resp.hex(' ').upper()}")
else:
print(" (无回应)")
ser.close()
print("\n──────")
print("用手机摄像头观察:")
print(" 红外LED = 白色透明或深紫色的小珠子")
print(" 发送时如果发光 → 手机屏幕里能看到紫白色闪光")
print(" 完全不亮 → 发送协议不对,板子根本没发射")