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