Files
toyingyi/ir_bruteforce.py
2026-06-09 10:19:38 +08:00

363 lines
12 KiB
Python
Raw Permalink 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.
# -*- 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()