初始化

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

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()