commit d03691451f819ac2347336fc154aa350164538dc Author: lhx Date: Tue Jun 9 10:19:38 2026 +0800 初始化 diff --git a/__pycache__/ir_send.cpython-313.pyc b/__pycache__/ir_send.cpython-313.pyc new file mode 100644 index 0000000..46295ef Binary files /dev/null and b/__pycache__/ir_send.cpython-313.pyc differ diff --git a/ir_bruteforce.py b/ir_bruteforce.py new file mode 100644 index 0000000..a190165 --- /dev/null +++ b/ir_bruteforce.py @@ -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() diff --git a/ir_diag.py b/ir_diag.py new file mode 100644 index 0000000..17358e5 --- /dev/null +++ b/ir_diag.py @@ -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() diff --git a/ir_receive.py b/ir_receive.py new file mode 100644 index 0000000..feac0ef --- /dev/null +++ b/ir_receive.py @@ -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() diff --git a/ir_send.py b/ir_send.py new file mode 100644 index 0000000..035772e --- /dev/null +++ b/ir_send.py @@ -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 发送自定义红外码 ║ +║ :learn <名> 学习新按键 ║ +║ :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() diff --git a/ir_try.py b/ir_try.py new file mode 100644 index 0000000..acf5a64 --- /dev/null +++ b/ir_try.py @@ -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(" 完全不亮 → 发送协议不对,板子根本没发射")