From fcb20a7fa30d10fbafdc5bcbf6b43f122f082e71 Mon Sep 17 00:00:00 2001 From: tonycao Date: Fri, 11 Sep 2026 08:12:06 +0800 Subject: [PATCH] =?UTF-8?q?hostdemo:=20RDM=20discovery=20+=20DMX=20control?= =?UTF-8?q?=20on=20real=20Butler=20.1=20=E2=80=94=20FULL=20WORKING=20(slfu?= =?UTF-8?q?ll=20recipe)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hostdemo.rs | 449 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 446 insertions(+), 3 deletions(-) diff --git a/src/hostdemo.rs b/src/hostdemo.rs index bece818..1ae698a 100644 --- a/src/hostdemo.rs +++ b/src/hostdemo.rs @@ -158,6 +158,442 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) { } if !dev_ok { println!("[1] 发现无应答, 终止"); return; } + // replay: verbatim 重放 SL 的完整会话帧流 (sym_startup t=101.3..136), 双阶段定位 RDM 门控 + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "replay" { + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let mut frames: Vec<(f32, String, u16, Vec)> = Vec::new(); + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let t: f32 = p[0].parse().unwrap(); + let dst = p[1].to_string(); + let port: u16 = p[2].parse().unwrap(); + let hex = &p[3]; + let mut b = Vec::with_capacity(hex.len() / 2); + for i in (0..hex.len()).step_by(2) { b.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + frames.push((t, dst, port, b)); + } + let t0 = frames[0].0; + for phase in ["A(无认领 verbatim)", "B(先认领再 verbatim)"] { + println!("[R] === 阶段 {}: 重放 {} 帧 / {:.1}s ===", phase, frames.len(), frames[frames.len() - 1].0 - t0); + if phase.starts_with('B') { + // 快速认领 + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + let ident = load_template("announce144"); + sock.send_to(&ident, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(300)); + let nonce = rng.next(); + let bc = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + 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(500)); + println!("[R] 认领已发 (B12=0x{:08x})", bc); + } + let start = Instant::now(); + let mut recv_n = 0u64; + let mut fi = 0; + while start.elapsed().as_secs_f32() < frames[frames.len() - 1].0 - t0 + 3.0 { + let now = start.elapsed().as_secs_f32(); + while fi < frames.len() && frames[fi].0 - t0 <= now { + let (_, d, port, b) = &frames[fi]; + let target_addr = format!("{}:{}", d, port); + let _ = sock.send_to(b, &target_addr); + fi += 1; + } + while let Ok((n, src)) = sock.recv_from(&mut buf) { + recv_n += 1; + let b = &buf[..n]; + println!(" [R] ← len={} src={} head={}", n, src, hexs(&b[..std::cmp::min(20, n)])); + } + std::thread::sleep(Duration::from_millis(2)); + } + println!("[R] 阶段 {} 结束: 收到 {} 帧", phase, recv_n); + std::thread::sleep(Duration::from_millis(1500)); + // 排空残留 + while sock.recv_from(&mut buf).is_ok() {} + } + return; + } + + // sls: session-continue — 不认领! 直接续用 SL 会话 (B12=0x4366937a), SL 实帧做模板 + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "sls" { + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let sb12: u32 = 0x4366937a; // SL 22:52 会话 B12 (Butler 会话状态绑定的值) + // 从重放清单提取模板: DMX 数据帧 / RDM branch 请求 + let mut base_dmx: Option> = None; + let mut base_rdm: Option> = None; + let mut max_ctr: u32 = 0; + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let hex = p[3]; + if hex.len() < 0x50 * 2 { continue; } + let b = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap(); + if hex.len() == 1084 * 2 && b(0x18) == 1 && b(0x20) == 1 && b(0x21) == 1 && b(0x3c) != 1 { + let ctr = g32(&hex.as_bytes(), 0); // placeholder never used + let _ = ctr; + let mut fb = Vec::with_capacity(1084); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + let c = u32::from_le_bytes([fb[0x1A], fb[0x1B], fb[0x1C], fb[0x1D]]); + if c > max_ctr { max_ctr = c; } + if base_dmx.is_none() { base_dmx = Some(fb); } + } + if hex.len() == 1084 * 2 && b(0x3c) == 1 && b(0x30) == 0 && b(0x50) == 0 && b(0x51) == 1 && base_rdm.is_none() { + let mut fb = Vec::with_capacity(1084); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + base_rdm = Some(fb); + } + } + let dmx_t = base_dmx.expect("no DMX template in replay file"); + let rdm_t = base_rdm.expect("no RDM template in replay file"); + let ctr0 = max_ctr + 10; + println!("[SLS] 会话续用: B12=0x{:08x}, DMX模板 counter 基线 {}", sb12, ctr0); + println!("[SLS] DMX模板 0x2E-0x3B: {}", hexs(&dmx_t[0x2E..0x3C])); + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], + ]; + let start = Instant::now(); + let mut ctr = ctr0; + let mut last_hb = Instant::now() - Duration::from_secs(10); + let mut last_rdm = Instant::now() - Duration::from_secs(10); + let mut step = 0usize; // 0=UN_MUTE, 1..=3=窄分支, 4+=GET + let mut addr_list: Vec = Vec::new(); + let mut resp_n = 0u64; + while start.elapsed() < Duration::from_secs(secs.max(60)) { + let t = start.elapsed().as_secs_f32(); + if last_hb.elapsed() >= Duration::from_secs(6) { + let _ = sock.send_to(&load_template("heartbeat"), &dst); + last_hb = Instant::now(); + } + // RDM 任务每 ~700ms 一步 + if t > 1.0 && last_rdm.elapsed() >= Duration::from_millis(700) { + last_rdm = Instant::now(); + let mut f = rdm_t.clone(); + f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); + let mut pd: Vec = Vec::new(); + let mut pid: u16 = 0x0001; + let mut dest = [0xffu8; 6]; + match step { + 0 => { pid = 0x0003; } // UN_MUTE 广播 + 1..=3 => { + let u = known[step - 1]; + let v = uid_u64(&u); + pd.extend_from_slice(&u64_uid(v - 1)); + pd.extend_from_slice(&u64_uid(v + 1)); + } + _ => { + // GET DEVICE_INFO 轮询已发现地址 + if (step - 4) < known.len() { + dest = known[step - 4]; + pid = 0x0060; + } else { step = 3; } // 循环重试 GET + } + } + let msg_len = 24 + pd.len(); + f[0x3C] = 0x01; + f[0x3D] = msg_len as u8; + f[0x3E..0x44].copy_from_slice(&dest); + f[0x4A] = (step * 7 + 1) as u8; + f[0x4F] = if pid == 0x0001 { 0x10 } else if pid == 0x0060 { 0x20 } else { 0x10 }; + f[0x50] = (pid >> 8) as u8; + f[0x51] = (pid & 0xFF) as u8; + f[0x52] = pd.len() as u8; + f[0x53..0x53 + pd.len()].copy_from_slice(&pd); + let mut sum: u16 = 0xCC; + for i in 0x3C..0x3C + msg_len { sum = sum.wrapping_add(f[i] as u16); } + let cs = 0x53 + pd.len(); + f[cs] = (sum >> 8) as u8; + f[cs + 1] = (sum & 0xFF) as u8; + let mut f2 = f.clone(); + f2[0x30] = 0x01; + f2[0x49] = f2[0x49].wrapping_sub(1); + let _ = sock.send_to(&f, &dst); + let _ = sock.send_to(&f2, &dst); + println!("[SLS] RDM step={} pid={:04x} 已发 (ctr={})", step, pid, ctr); + step += 1; + } + // 收包 + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + resp_n += 1; + if let Some((is_pulse, d)) = rdm_region(b) { + if is_pulse { + if let Some(uid) = decode_pulse(d) { + println!(" [SLS] ★脉冲 UID={}", uid_s(&uid)); + } else { + println!(" [SLS] 脉冲(冲突/损坏)"); + } + } else if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]); + println!(" [SLS] ★DEVICE_INFO srcUID={} DMX地址={} 通道={}", uid_s(&[d[9], d[10], d[11], d[12], d[13], d[14]]), a, fp); + if !addr_list.contains(&a) { addr_list.push(a); } + } else { + println!(" [SLS] RDM应答 len={}", d.len()); + } + } + } + // DMX @30fps: 发现完成前黑场, 之后呼吸 + let mut p1 = [0u8; 512]; + if !addr_list.is_empty() { + for (k, &a) in addr_list.iter().enumerate() { + let phase = t * 1.2 + k as f32 * 2.1; + let v = (((phase.sin() * 0.5 + 0.5) * 255.0) * ((t - 1.0) * 0.5).min(1.0).max(0.0)) as u8; + let ch = a.max(1) - 1; + if ch < 512 { p1[ch as usize] = v; } + } + } + let r1 = rng.next(); + let r2 = rng.next(); + let mut f = dmx_t.clone(); + f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); + f[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, sb12)); + f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], r2, sb12)); + let _ = sock.send_to(&f, &dst); + ctr += 1; + std::thread::sleep(Duration::from_millis(33)); + } + println!("[SLS] 结束: 应答 {} 帧, 控制地址 {:?}", resp_n, addr_list); + return; + } + + // hybrid: 认领 + verbatim SL DMX 帧 + 我们构造的 RDM 帧 — 二分定位毒帧 + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "hybrid" { + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let mut dmxf: Vec> = Vec::new(); // verbatim DMX 帧 + let mut base_rdm: Option> = None; + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let hex = p[3]; + if hex.len() != 1084 * 2 { continue; } + let b = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap(); + if b(0x18) == 1 && b(0x20) == 1 && b(0x21) == 1 && b(0x3c) != 1 { + let mut fb = Vec::new(); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + dmxf.push(fb); + } + if base_rdm.is_none() && b(0x3c) == 1 && b(0x30) == 0 && b(0x50) == 0 && b(0x51) == 1 { + let mut fb = Vec::new(); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + base_rdm = Some(fb); + } + } + // 认领 (announce144 → AECH) + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + sock.send_to(&load_template("announce144"), &dst).unwrap(); + std::thread::sleep(Duration::from_millis(300)); + let nonce = rng.next(); + let bc = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + 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)); + println!("[H] 认领完成 B12=0x{:08x}; verbatim DMX {} 帧 + 我们的 RDM", bc, dmxf.len()); + let rdm_t = base_rdm.expect("no RDM template"); + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], + ]; + let start = Instant::now(); + let mut di = 0usize; + let mut last_rdm = Instant::now() - Duration::from_secs(10); + let mut step = 0usize; + let mut addr_list: Vec = Vec::new(); + let mut resp_n = 0u64; + while start.elapsed() < Duration::from_secs(secs.max(90)) { + let t = start.elapsed().as_secs_f32(); + // verbatim DMX 循环 + let f = &dmxf[di % dmxf.len()]; + di += 1; + let _ = sock.send_to(f, &dst); + if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); } + // RDM 任务 + if t > 2.0 && last_rdm.elapsed() >= Duration::from_millis(700) { + last_rdm = Instant::now(); + let mut f = rdm_t.clone(); + let mut pd: Vec = Vec::new(); + let mut pid: u16 = 0x0001; + let mut dest = [0xffu8; 6]; + let mut cc = 0x10u8; + match step { + 0 => { pid = 0x0003; } + 1..=3 => { + let v = uid_u64(&known[step - 1]); + pd.extend_from_slice(&u64_uid(v - 1)); + pd.extend_from_slice(&u64_uid(v + 1)); + } + _ => { + if (step - 4) < known.len() { dest = known[step - 4]; pid = 0x0060; cc = 0x20; } + else { step = 3; } + } + } + let msg_len = 24 + pd.len(); + f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes()); + f[0x3D] = msg_len as u8; + f[0x3E..0x44].copy_from_slice(&dest); + f[0x4A] = (step * 7 + 1) as u8; + f[0x4F] = cc; + f[0x50] = (pid >> 8) as u8; + f[0x51] = (pid & 0xFF) as u8; + f[0x52] = pd.len() as u8; + f[0x53..0x53 + pd.len()].copy_from_slice(&pd); + let mut sum: u16 = 0xCC; + for i in 0x3C..0x3C + msg_len { sum = sum.wrapping_add(f[i] as u16); } + let cs = 0x53 + pd.len(); + f[cs] = (sum >> 8) as u8; + f[cs + 1] = (sum & 0xFF) as u8; + let mut f2 = f.clone(); + f2[0x30] = 0x01; + f2[0x49] = f2[0x49].wrapping_sub(1); + let _ = sock.send_to(&f, &dst); + let _ = sock.send_to(&f2, &dst); + println!("[H] RDM step={} pid={:04x}", step, pid); + step += 1; + } + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + resp_n += 1; + if let Some((is_pulse, d)) = rdm_region(b) { + if is_pulse { + if let Some(uid) = decode_pulse(d) { println!(" [H] ★脉冲 UID={}", uid_s(&uid)); } + else { println!(" [H] 脉冲(冲突)"); } + } else if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]); + println!(" [H] ★DEVICE_INFO srcUID={} 地址={} 通道={}", uid_s(&[d[9], d[10], d[11], d[12], d[13], d[14]]), a, fp); + if !addr_list.contains(&a) { addr_list.push(a); } + } else { + println!(" [H] RDM应答 len={}", d.len()); + } + } + } + std::thread::sleep(Duration::from_millis(33)); + } + println!("[H] 结束: 应答 {} 帧, 地址 {:?}", resp_n, addr_list); + return; + } + + // slfull: 认领 + verbatim 重放 SL 会话(循环) + 被动解码 RDM 应答 → 发现灯具并打印地址 + // (绕过未破解的变长 RDM 帧结构: 用 SL 原帧当查询, 我们解码应答) + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "slfull" { + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let mut frames: Vec<(f32, String, u16, Vec)> = Vec::new(); + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let t: f32 = p[0].parse().unwrap(); + let dst = p[1].to_string(); + let port: u16 = p[2].parse().unwrap(); + let hex = p[3]; + let mut b = Vec::new(); + for i in (0..hex.len()).step_by(2) { b.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + frames.push((t, dst, port, b)); + } + let t0 = frames[0].0; + let dur = frames[frames.len() - 1].0 - t0; + // 认领 + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + sock.send_to(&load_template("announce144"), &dst).unwrap(); + std::thread::sleep(Duration::from_millis(300)); + let nonce = rng.next(); + let bc = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + 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(500)); + println!("[F] 认领完成; 开始循环重放 {} 帧 ({:.1}s/轮), 被动解码 RDM 应答", frames.len(), dur); + let start = Instant::now(); + let mut seen_uids: Vec<[u8; 6]> = Vec::new(); + let mut addr_map: Vec<([u8; 6], u16)> = Vec::new(); + let mut resp_n = 0u64; + let mut pat_ctr: u32 = 0; + while start.elapsed() < Duration::from_secs(secs.max(120)) { + let play_start = Instant::now(); + for (t, d, port, b) in frames.iter() { + let target_t = Duration::from_secs_f32(t - t0); + let now = play_start.elapsed(); + if target_t > now { std::thread::sleep(target_t - now); } + let addr = format!("{}:{}", d, port); + // ★DMX 数据帧: 替换为我们自己的呼吸图案 (用认领 B12 加密 → Butler 解密后上灯) + let mut fb = b.clone(); + if fb.len() == 1084 && fb[0x18] == 1 && fb[0x20] == 1 && fb[0x21] == 1 && fb[0x3C] != 1 { + pat_ctr += 1; + let tt = pat_ctr as f32 / 30.0; + let mut p1 = [0u8; 512]; + for (k, &a) in [101u16, 123, 1].iter().enumerate() { + let phase = tt * 1.0 + k as f32 * 2.1; + let v = (((phase.sin() * 0.5 + 0.5) * 220.0) * (tt * 0.3).min(1.0)) as u8; + p1[(a - 1) as usize] = v; + } + let r1 = rng.next(); + let r2 = rng.next(); + fb[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + fb[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + fb[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, bc)); + fb[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], r2, bc)); + } + let _ = sock.send_to(&fb, &addr); + // 收包 + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let bb = &buf[..n]; + resp_n += 1; + if let Some((is_pulse, dd)) = rdm_region(bb) { + if is_pulse { + if let Some(uid) = decode_pulse(dd) { + if !seen_uids.contains(&uid) { + seen_uids.push(uid); + println!(" [F] ★发现灯具 UID={}", uid_s(&uid)); + } + } + } else if dd.len() >= 0x18 + 19 && dd[0] == 0xCC && dd[0x15..0x17] == [0x00, 0x60] { + let src_uid = [dd[9], dd[10], dd[11], dd[12], dd[13], dd[14]]; + let a = u16::from_be_bytes([dd[0x18 + 14], dd[0x18 + 15]]); + let fp = u16::from_be_bytes([dd[0x18 + 10], dd[0x18 + 11]]); + if !addr_map.iter().any(|(u, _)| *u == src_uid) { + addr_map.push((src_uid, a)); + println!(" [F] ★{} → DMX地址 {} ({}通道)", uid_s(&src_uid), a, fp); + } + } else if dd.len() >= 0x18 + 2 && dd[0] == 0xCC && dd[0x15..0x17] == [0x00, 0xF0] { + let src_uid = [dd[9], dd[10], dd[11], dd[12], dd[13], dd[14]]; + let a = u16::from_be_bytes([dd[0x18], dd[0x19]]); + if !addr_map.iter().any(|(u, _)| *u == src_uid) { + addr_map.push((src_uid, a)); + println!(" [F] ★{} → DMX_START_ADDRESS={}", uid_s(&src_uid), a); + } + } + } + } + } + } + println!("[F] 结束: 应答 {} 帧; 灯具 {} 台: {:?}; 地址: {:?}", resp_n, seen_uids.len(), + seen_uids.iter().map(uid_s).collect::>(), addr_map); + return; + } + let mut b12 = rng.next(); let mut claim_ok = false; @@ -723,8 +1159,12 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u let mut tn: u8 = rng.byte(); let mut qseq: u16 = 1; let mut seq: u32 = 1; - // 发现任务队列: (dest, cc, pid, pd) — None = 分支查询由 walk 状态机生成 + // 发现任务队列: ★窄分支优先 (已知灯具各一个只含自己的分支 — Butler 可能拒转全范围查询) let mut stack: Vec<(u64, u64)> = vec![(0, 0xFFFF_FFFF_FFFF)]; + for u in known.iter() { + let v = uid_u64(u); + stack.push((v - 1, v + 1)); + } let mut get_queue: Vec<[u8; 6]> = Vec::new(); let mut muted: Vec<[u8; 6]> = Vec::new(); let start = Instant::now(); @@ -757,8 +1197,8 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u last_key_t = t; } - // --- RDM 任务 (每 ~100ms 一发; ★SL 实测: 认领后 27.5s 纯 DMX, RDM 过早发可能重置武装计时) --- - if discovering && t > 30.0 && last_rdm.elapsed() >= Duration::from_millis(100) { + // --- RDM 任务 (每 ~100ms 一发; 窄分支优先, t>3s 即开始快速定位问题) --- + if discovering && t > 3.0 && last_rdm.elapsed() >= Duration::from_millis(100) { last_rdm = Instant::now(); if collecting && Instant::now() >= collect_until { // 本分支收集结束 → 判定 @@ -804,6 +1244,7 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u let _ = sock.send_to(&f, dst); let mut f2 = f.clone(); f2[0x30] = 0x01; + f2[0x49] = f2[0x49].wrapping_sub(1); let _ = sock.send_to(&f2, dst); } } @@ -830,6 +1271,8 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u let _ = sock.send_to(&f, dst); let mut f2 = f.clone(); f2[0x30] = 0x01; + // flag=01 副本的 srcUID 末字节 = fe (SL 实测: ff/fe 成对) + f2[0x49] = f2[0x49].wrapping_sub(1); let _ = sock.send_to(&f2, dst); current_branch = Some((lo, hi)); collecting = true;