From 8938b15691c7793ee9f9ec1c1b9d4636eadd24b7 Mon Sep 17 00:00:00 2001 From: OpenCode Date: Fri, 11 Sep 2026 13:53:21 +0800 Subject: [PATCH] fix: dual-port DMX output in show mode; AERE claim verification; SHOW_DIAG mode - Root cause of 'steady white': fixture on Butler port2, our data only went to port1 (port data regions are independent: port1 @0x3C, port2 @0x23C) - show mode now mirrors pattern to both ports; SHOW_DIAG=1 gives 3s off/on blink test - Claim now verifies AERE response and warns on failure - Docs updated with port layout note; README updated --- README.md | 2 +- docs/enet_protocol_current.md | 5 ++++- src/analyze.rs | 28 ++++++++++++++++++++++++++++ src/hostdemo.rs | 32 +++++++++++++++++++++++++++----- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a82aa11..73b55f8 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ $env:HOSTDEMO_MODE = "show" .\target\release\enet-validator.exe hostdemo 192.168.123.1 600 ``` -- DMX:地址 1-32 彩虹呼吸渐变(HMKJ 32 通道灯具),**零模板纯自构造帧**(`build_dmx_frame`) +- DMX:地址 1-32 彩虹呼吸渐变(HMKJ 32 通道灯具),**零模板纯自构造帧**(`build_dmx_frame`),**port1+port2 双口同时输出**(灯具接任一口均可控) - RDM:每 700ms 轮询 3 台已知 UID,实时打印 `DEVICE_INFO` 与 `DMX_START_ADDRESS`,**纯自构造请求帧**(`build_rdm_frame2`) - 运行无需任何抓包/replay 文件;DMX/RDM 帧全字段已破解(见 docs/enet_protocol_current.md) diff --git a/docs/enet_protocol_current.md b/docs/enet_protocol_current.md index 8fec844..c9ebf85 100644 --- a/docs/enet_protocol_current.md +++ b/docs/enet_protocol_current.md @@ -32,7 +32,10 @@ | 0x22-0x25 | r1 u32 LE | 随机数,port1 加密种子 | | 0x26-0x29 | r2 u32 LE | 随机数,port2 加密种子 | | 0x2A-0x2D | 00 00 00 00 | 常量 | -| 0x2E-0x2F | 01 00 | 常量(实测 02 也被接受 → Butler 不校验) | +| 0x2E-0x2F | 01 00 | 输出口对序号(SL 抓包=01;实测 01/02 均被接受) | + +> ⚠ **两个 DMX 口数据区独立**:port1 @0x3C、port2 @0x23C。灯具接在哪个口就吃哪个口的数据。 +> 实测灯具在 port2 时只发 port1 会导致灯具保持旧值/兜底白色——**show 模式已改为双口同时输出**。 | 0x30-0x31 | 00 00 | 常量 | | 0x32-0x33 | 01 00 | 常量 | | 0x34-0x35 | 00 02 | port1 数据长度 = 512(u16 LE) | diff --git a/src/analyze.rs b/src/analyze.rs index 5888979..60c12ad 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -210,6 +210,34 @@ pub fn run(args: &[String]) { "hdr" => hdr(), "rdm" => rdm(), "ctr" => ctr(), + // gendmx: 同输入生成 从零构造 vs dmx_a模板补丁 两版帧并逐字节 diff + "gendmx" => { + let p1 = [0x11u8; 512]; + let p2 = [0u8; 512]; + let r1: u32 = 0x15198c1a; + let r2: u32 = 0x1676d663; + let b12: u32 = 0x11223344; + let ctr: u32 = 0x197; + let gen = crate::hostdemo::build_dmx_frame(&p1, &p2, r1, r2, b12, ctr); + // 模板版: dmx_a + 同样的补丁 (复刻已验证工作版本) + let mut tpl = load_hex("samples/dmx_a_1126.hex"); + tpl[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); + tpl[0x2E] = 2; tpl[0x2F] = 0; + tpl[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + tpl[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + let enc1 = crate::aech::agios_dmx_encrypt(&p1, r1, b12); + let enc2 = crate::aech::agios_dmx_encrypt(&p2, r2, b12); + tpl[0x3C..0x23C].copy_from_slice(&enc1); + tpl[0x23C..0x43C].copy_from_slice(&enc2); + println!("gen len={} tpl len={}", gen.len(), tpl.len()); + let n = gen.len().min(tpl.len()); + let diffs: Vec = (0..n).filter(|&i| gen[i] != tpl[i]).collect(); + println!("差异 {} 处:", diffs.len()); + for i in &diffs { + println!(" 0x{:03x}: gen={:02x} tpl={:02x}", i, gen[*i], tpl[*i]); + } + if diffs.is_empty() { println!("两版逐字节一致"); } + } _ => println!("未知 analyze 子命令 {}", args[0]), } } diff --git a/src/hostdemo.rs b/src/hostdemo.rs index b0eb7fc..fa451b4 100644 --- a/src/hostdemo.rs +++ b/src/hostdemo.rs @@ -104,7 +104,7 @@ fn build_rdm_frame(dest: &[u8; 6], tn: u8, qseq: u16, cc: u8, pid: u16, pd: &[u8 /// ★从零构造 DMX 数据帧 (1084B, 全字段已破解 — analyze hdr 实证): /// 帧头全部常量已知, 仅 0x1A 计数器(+1/帧) 与 0x22/0x26 r1/r2(加密随机) 变化 -fn build_dmx_frame(p1: &[u8; 512], p2: &[u8; 512], r1: u32, r2: u32, b12: u32, ctr: u32) -> Vec { +pub fn build_dmx_frame(p1: &[u8; 512], p2: &[u8; 512], r1: u32, r2: u32, b12: u32, ctr: u32) -> Vec { let mut f = vec![0u8; 1084]; f[0..4].copy_from_slice(b"ENet"); f[4] = 4; // type=4 @@ -116,7 +116,7 @@ fn build_dmx_frame(p1: &[u8; 512], p2: &[u8; 512], r1: u32, r2: u32, b12: u32, c f[0x20] = 1; f[0x21] = 1; // 端口使能 mask: port1+port2 f[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); - f[0x2E] = 1; // 常量 (实测 01; 02 也能被接受) + f[0x2E] = 2; // 输出口对序号 (SL 抓包=01, 本机实测 01/02 均可; 关键在数据区按端口分开) f[0x32] = 1; // 常量 f[0x35] = 2; f[0x37] = 2; // port1/port2 数据长度 = 512 (u16 LE ×2) f[0x38] = 1; // 常量 01 00 00 00 @@ -482,7 +482,22 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) { ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes()); sock.send_to(&ch, &dst).unwrap(); - std::thread::sleep(Duration::from_millis(400)); + // ★验证 AERE 应答 (RDM 不加密, 认领失败 RDM 照样通但 DMX 解密失败 → 灯白色兜底) + let mut aere_ok = false; + let w = Instant::now(); + while w.elapsed() < Duration::from_millis(700) { + if let Ok((n, _)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + if n > 0xC4 && b.windows(4).any(|x| x == b"AERE") { + let resp = g32(b, 0xC0); + let want = agios_g(nonce); + aere_ok = resp == want; + println!("[{}] AERE resp=0x{:08x} want=0x{:08x} {}", label, resp, want, if aere_ok { "★PASS" } else { "FAIL" }); + break; + } + } + } + if !aere_ok { println!("[{}] ⚠ 未收到有效 AERE — 认领可能失败, DMX 将无法解密!", label); } println!("[{}] 认领完成 B12=0x{:08x}; {}", label, bc, if is_show { "地址1-32 炫彩呼吸 (零模板纯自构造 DMX+RDM)" } else { "verbatim DMX + 模板 RDM" }); let rdm_t: Option> = base_rdm; @@ -515,7 +530,11 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) { // verbatim DMX 循环 // DMX 数据替换 let mut p1 = [0u8; 512]; - if is_show { + if std::env::var("SHOW_DIAG").unwrap_or_default() == "1" { + // 诊断: ch1-32 全灭3s / 全亮3s 交替 — 验证 DMX 是否到达灯具 + let v = if (t as u32 / 3) % 2 == 0 { 0u8 } else { 255u8 }; + for i in 0..32 { p1[i] = v; } + } else if is_show { // show: 地址 1-32 炫彩呼吸波 (周期放慢, 更舒缓) let breathe = (t * 0.45).sin() * 0.5 + 0.5; for i in 0..32usize { @@ -531,9 +550,12 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) { } let rr1 = rng.next(); let rr2 = rng.next(); + // show 模式: 图案同时输出到 port1+port2 (灯具可能在任一口, 实测 port2 出灯) + let mut p2 = [0u8; 512]; + if is_show || std::env::var("SHOW_PORT2").unwrap_or_default() == "1" { p2 = p1; } let f = if is_show { // ★纯自构造 DMX 帧 (零模板) - build_dmx_frame(&p1, &[0u8; 512], rr1, rr2, bc, di as u32) + build_dmx_frame(&p1, &p2, rr1, rr2, bc, di as u32) } else { let mut fx = dmxf[di % dmxf.len()].clone(); fx[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());