enet-validator/src/hostdemo.rs
tonycao 0a183e449c hostdemo: full parameter sweep + periodic RDM status polling in session_loop
After zero-pre-baked discovery, session_loop now sweeps each fixture
(DEVICE_INFO with E1.20 PD decode + raw hex, DMX_START_ADDRESS,
DEVICE_LABEL / MODEL_DESCRIPTION / SOFTWARE_VERSION / MANUFACTURER_LABEL)
with a 2-round retry for the flaky RDM window arm, prints a per-fixture
parameter summary, then enters a 6s status-poll cycle: one compact line
per fixture per cycle, address/footprint changes reported as alarms, and
playback follows a changed DMX address automatically.  Duplicate uplinks
from the flag=00/01 pair copies are deduped; the playback address list
is kept per-UID (no more duplicate control-address entries).
2026-09-14 21:04:45 +08:00

2104 lines
110 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! hostdemo.rs — 主机侧等效示例程序 (2026-09-10): 认领真机 + RDM 发现 + 加密 DMX 控制
//!
//! 全流程不依赖 SymphoLight:
//! 1. 发现 (type=1, 180B) → 217B 应答
//! 2. sub=01 宣告 → 89B 注册应答
//! 3. AECH 认领 ×3 轮 (nonce/B12 自选, AERE[0xC0]==G(nonce) 逐轮验证)
//! ★ 真机 .1 只应答源 IP=192.168.123.113 的 AECH (固件过滤, §3.7)
//! 4. RDM 发现: E1.20 DISC_UNIQUE_BRANCH 二分遍历 + DISC 脉冲解码 (§5.4 逆运算)
//! → 每 UID 发 DISC_MUTE(0002) 收 70B ACK → GET DEVICE_INFO/DMX_START_ADDRESS/DEVICE_LABEL
//! 5. DMX 控制: 30fps zPprv0TXRRW 加密帧 (§4.1), 按发现的灯具地址做呼吸图案
//!
//! 注意: 真机控灯! 且 SymphoLight 若同时运行会插黑场帧 (两主机流交替 → 闪烁)。
use std::net::UdpSocket;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::aech::{agios_dmx_encrypt, agios_g};
use crate::enet::{load_template, open_reuse};
const HOST_RDM_UID: [u8; 6] = [0x45, 0x3a, 0x7f, 0xff, 0xff, 0xff]; // 与真主机一致 (§5.3)
const BCAST_UID: [u8; 6] = [0xff; 6];
/// 真机冷启动实测: AECH 认领前主机先发 102B type=2 身份帧 (cold2 @38.087)
/// 主机 IP@0x1C (LE), MAC@0x20 (模板值即本机 123 网卡)
const IDENT102: [u8; 102] = [
0x45, 0x4e, 0x65, 0x74, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x7c, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x66, 0x00, 0x00, 0x00, 0x01, 0x00, 0x71, 0x7b, 0xa8, 0xc0, 0x30, 0x0e,
0xd5, 0x59, 0xbb, 0x36, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
/// ★type=02 sub=07 绑定帧 (52B, sym_claim2 @351.1/353.4 — 认领前 2s 各一发):
/// 主机IP+MAC@0x1A/0x1E, 设备IP+MAC@0x25/0x29, ffff/0000@0x2F (两发唯一差异), 6400 f401@0x31
/// (0x64=100, 0x01f4=500 — 疑似刷新率参数, 原样保留)
fn build_bind52(flag16: [u8; 2]) -> Vec<u8> {
let mut f = Vec::with_capacity(52);
f.extend_from_slice(b"ENet");
f.extend_from_slice(&2u32.to_le_bytes());
f.extend_from_slice(&[0x00, 0x00, 0x1c, 0x7c]);
f.extend_from_slice(&[0u8; 4]);
f.extend_from_slice(&[0x00, 0x00, 0x01, 0x00]);
f.extend_from_slice(&52u32.to_le_bytes());
f.extend_from_slice(&[0x07, 0x00]); // sub=07
f.extend_from_slice(&[0x71, 0x7b, 0xa8, 0xc0]); // 占位: 主机IP 逆序字节 (patch)
f.extend_from_slice(&[0x30, 0x0e, 0xd5, 0x59, 0xbb, 0x36]); // 主机MAC
f.push(0x00);
f.extend_from_slice(&[0x01, 0x7b, 0xa8, 0xc0]); // 设备IP LE = 192.168.123.1
f.extend_from_slice(&[0x00, 0x16, 0x1c, 0xf1, 0x2e, 0x39]); // 设备MAC
f.extend_from_slice(&flag16);
f.extend_from_slice(&[0x64, 0x00, 0xf4, 0x01]);
f
}
fn g16(b: &[u8], o: usize) -> u16 { u16::from_le_bytes([b[o], b[o + 1]]) }
fn g32(b: &[u8], o: usize) -> u32 { u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]) }
fn uid_u64(u: &[u8; 6]) -> u64 { u64::from_be_bytes([0, 0, u[0], u[1], u[2], u[3], u[4], u[5]]) }
fn u64_uid(v: u64) -> [u8; 6] { let b = v.to_be_bytes(); [b[2], b[3], b[4], b[5], b[6], b[7]] }
fn hexs(b: &[u8]) -> String { b.iter().map(|x| format!("{:02x}", x)).collect() }
fn uid_s(u: &[u8; 6]) -> String { hexs(u) }
struct Rng(u32);
impl Rng {
fn new() -> Self { Rng(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_nanos() | 1) }
fn next(&mut self) -> u32 { self.0 ^= self.0 << 13; self.0 ^= self.0 >> 17; self.0 ^= self.0 << 5; self.0 & 0x7FFF_FFFF }
fn byte(&mut self) -> u8 { (self.next() >> 8) as u8 }
}
/// E1.20 请求构建: 在 1084B 事件帧模板上 patch RDM 消息 (§4.2/§5.3)
/// frame: 0x22 qseq(LE), 0x34=msgLen+1(LE), 0x3C 起 = SubSC/msgLen/destUID/srcUID/TN/...
fn build_rdm_frame(dest: &[u8; 6], tn: u8, qseq: u16, cc: u8, pid: u16, pd: &[u8]) -> Vec<u8> {
let mut f = load_template("rdm_disc_req"); // 1084B, flag=00 独立帧形态
let msg_len = 24 + pd.len(); // CC..PD 末 (不含 checksum)
// ★RDM 请求帧头与 DMX 帧不同 (SL 实时帧对照): 计数器/随机槽位必须清零
f[0x1A..0x20].copy_from_slice(&[0u8; 6]); // 0x1A 计数器(会话内从0) + 0x1E-0x1F
f[0x24..0x2A].copy_from_slice(&[0u8; 6]); // 0x24-0x29 (DMX 帧的 r1/r2 槽位 = 0)
f[0x22] = (qseq & 0xFF) as u8;
f[0x23] = (qseq >> 8) as u8;
f[0x30] = 0; // flag=00 独立查询帧 (真机只应答此类)
f[0x34..0x38].copy_from_slice(&((msg_len + 1) as u32).to_le_bytes());
f[0x3C] = 0x01; // SubSC
f[0x3D] = msg_len as u8;
f[0x3E..0x44].copy_from_slice(dest);
f[0x44..0x4A].copy_from_slice(&HOST_RDM_UID);
f[0x4A] = tn;
f[0x4B] = 0x00; // 独立帧 port 字段=00 (模板实证; flag=01 内嵌副本才是 01)
f[0x4C] = 0x00; // msgCount
f[0x4D] = 0; f[0x4E] = 0; // subDevice
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);
// checksum: 含隐式 0xCC 起始字节, Σ(0x3C..0x53+PDL) (不含 checksum 槽位自身!), BE
// ★历史 bug: 曾写成 0x3C..0x3C+msg_len 多算 1 字节(含模板残留旧 checksum) → Butler 不应答
let mut sum: u16 = 0xCC;
for i in 0x3C..0x53 + pd.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;
f
}
/// ★从零构造 DMX 数据帧 (1084B, 全字段已破解 — analyze hdr 实证):
/// 帧头全部常量已知, 仅 0x1A 计数器(+1/帧) 与 0x22/0x26 r1/r2(加密随机) 变化
pub fn build_dmx_frame(p1: &[u8; 512], p2: &[u8; 512], r1: u32, r2: u32, b12: u32, ctr: u32) -> Vec<u8> {
let mut f = vec![0u8; 1084];
f[0..4].copy_from_slice(b"ENet");
f[4] = 4; // type=4
f[0x0A] = 0x1c; f[0x0B] = 0x7c; // 目标端口 31772 (LE)
f[0x12] = 1; // 常量 01 00
f[0x14] = 0x3c; f[0x15] = 0x04; // 帧长 1084 (u16 LE)
f[0x18] = 1; // 常量 01 00
f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); // 帧计数器 (+1/帧, 任意起始)
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] = 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
f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(p1, r1, b12));
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(p2, r2, b12));
f
}
/// ★从零构造 RDM 请求帧 (1084B, 全字段已破解 — analyze rdm 实证):
/// 计数器恒 0; 0x20=02 为 RDM 请求标志; 0x2A=0xCC 常量; checksum=0xCC+Σ(0x3C..0x53+PDL) @0x53+PDL
/// msgLen=24+PDL (含隐式 0xCC 起始码); checksum 后任意填充 (Butler 不读)
fn build_rdm_frame2(dest: &[u8; 6], tn: u8, qseq: u16, cc: u8, pid: u16, pd: &[u8]) -> Vec<u8> {
let msg_len = 24 + pd.len();
let mut f = vec![0u8; 1084];
f[0..4].copy_from_slice(b"ENet");
f[4] = 4;
f[0x0A] = 0x1c; f[0x0B] = 0x7c;
f[0x12] = 1;
f[0x14] = 0x3c; f[0x15] = 0x04;
f[0x18] = 1;
// 0x1A-0x1F: 计数器恒 0
f[0x20] = 2; // RDM 请求标志
f[0x22..0x24].copy_from_slice(&qseq.to_le_bytes());
f[0x2A] = 0xCC; // 常量
f[0x2E] = 1;
// 0x30: 00 主帧 (成对副本=01)
f[0x34..0x38].copy_from_slice(&((msg_len + 1) as u32).to_le_bytes());
f[0x38] = 1;
// RDM 消息 @0x3C: SubSC/msgLen/dest/src/TN/port/msgCount/subDev/CC/PID/PDL/PD
f[0x3C] = 1;
f[0x3D] = msg_len as u8;
f[0x3E..0x44].copy_from_slice(dest);
f[0x44..0x4A].copy_from_slice(&HOST_RDM_UID);
f[0x4A] = tn;
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..0x53 + pd.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;
f
}
/// RDM 成对副本 (SL 实测: flag@0x30=01, port@0x4B=01, srcUID 末字节-1, checksum 不重算)
fn rdm_pair_copy(f: &[u8]) -> Vec<u8> {
let mut f2 = f.to_vec();
f2[0x30] = 0x01;
f2[0x4B] = 0x01;
f2[0x49] = f2[0x49].wrapping_sub(1);
f2
}
/// DISC 脉冲解码 (§5.4 逆运算): data = RDM 数据区 (0x2A 起), 返回 Some(UID) 或 None(冲突/非法)
fn decode_pulse(d: &[u8]) -> Option<[u8; 6]> {
if d.len() < 17 || d[0] != 0xAA { return None; }
let mut uid = [0u8; 6];
let mut sum: u16 = 0;
for i in 0..6 {
let b1 = d[1 + 2 * i];
let b2 = d[2 + 2 * i];
// 编码有效性: b1 高位必含 0xAA 模式, b2 必含 0x55 — 违例即多机冲突
if b1 & 0xAA != 0xAA || b2 & 0x55 != 0x55 { return None; }
uid[i] = (b1 & 0x55) | (b2 & 0xAA);
sum = sum.wrapping_add(b1 as u16).wrapping_add(b2 as u16);
}
let cs_hi = (d[13] & 0x55) | (d[14] & 0xAA);
let cs_lo = (d[15] & 0x55) | (d[16] & 0xAA);
let cs = ((cs_hi as u16) << 8) | cs_lo as u16;
if cs != sum { return None; } // 冲突/损坏
Some(uid)
}
/// 从 Butler→主机 应答帧提取 RDM 数据区 (§5.1): 返回 (is_pulse, data)
fn rdm_region(b: &[u8]) -> Option<(bool, &[u8])> {
if b.len() < 0x2E || &b[0..4] != b"ENet" || g32(b, 4) != 4 { return None; }
if g16(b, 0x18) != 0x0002 { return None; }
if &b[0x1A..0x1E] != b"Butl" { return None; }
let rlen = g16(b, 0x28) as usize;
// rlen=0 = 42B 事件计数遥测帧 (真机有事件时 ~115ms 一帧), 非 RDM 载荷
if rlen == 0 || b.len() < 0x2A + rlen { return None; }
let d = &b[0x2A..0x2A + rlen];
Some((d[0] == 0xAA, d))
}
pub fn run(target: &str, bind_ip: &str, secs: u64) {
let dst = format!("{}:31772", target);
// HOSTDEMO_LISTEN: 实际 socket 绑定地址 (默认=bind_ip:31772)。
// 跨网段经 udprelay 中继时, Ubuntu 侧须绑本地地址 (如 0.0.0.0:31772), 帧内宣告 IP 仍为 bind_ip (123.113)
let listen = std::env::var("HOSTDEMO_LISTEN").unwrap_or_else(|_| format!("{}:31772", bind_ip));
let sock = open_reuse(&listen); // 源 123.113:31772 (真机过滤+主机惯例)
sock.set_read_timeout(Some(Duration::from_millis(5))).unwrap();
let mut rng = Rng::new();
let mut buf = [0u8; 4096];
println!("=== 主机侧等效示例: 目标 {} (源 {}, 监听 {}) ===", dst, bind_ip, listen);
// ---- 1) 发现 ----
let disc = load_template("discovery_req");
sock.send_to(&disc, &dst).unwrap();
let mut dev_ok = false;
for _ in 0..150 {
if let Ok((n, src)) = sock.recv_from(&mut buf) {
let b = &buf[..n];
if n >= 200 && &b[0..4] == b"ENet" && g32(b, 4) == 1 {
println!("[1] 发现应答 {}B from {} (Butler 在线)", n, src);
dev_ok = true;
break;
}
}
}
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<u8>)> = 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<u8> = 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<Vec<u8>> = None;
let mut base_rdm: Option<Vec<u8>> = 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<u16> = 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<u8> = 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 / show: 认领 + 用 SL 实帧骨架, 替换为我们的 DMX 数据 + 我们构造的 RDM
// 区别: hybrid 保留原 SL 数据地址(101/123/1 可选替换); show 固定地址1-32炫彩呼吸
let mode = std::env::var("HOSTDEMO_MODE").unwrap_or_default();
if mode == "hybrid" || mode == "show" {
let is_show = mode == "show";
let label = if is_show { "SHOW" } else { "H" };
// show: 全字段已破解, 零模板纯自构造; hybrid: 仍用 SL 实帧骨架做对照
let mut dmxf: Vec<Vec<u8>> = Vec::new();
let mut base_rdm: Option<Vec<u8>> = None;
if !is_show {
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");
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; claim 帧纯自构造: 不透明区填 0, CLAIM_ZERO 消融已证实 Butler 不校验)
let o2: Vec<u8> = 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 o2a = [o2[0], o2[1], o2[2], o2[3]];
let ch = build_claim_frame(o2a, nonce, bc);
sock.send_to(&ch, &dst).unwrap();
// ★验证 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" });
// ★show 模式: 认领后进入统一会话环 — 零预制全空间二分发现 + 按发现地址播放。
// (旧 show 轮询循环依赖非空 known 清单; 67866e2 起 known 默认为空,
// step 状态机在 0↔1 死循环并发出 PDL=0 的畸形 DUB, DUB 池应答因此全静默
// — 2026-09-14 本机 Linux 版定位)
if is_show {
let run_secs = if secs == 0 { u64::MAX } else { secs };
println!("[SHOW] 进入统一会话环: 零预制二分发现 + 播放 ({}s)", if secs == 0 { 0 } else { secs });
send_key_burst(&sock);
session_loop(&sock, &dst, target, &mut rng, bc, run_secs);
return;
}
let rdm_t: Option<Vec<u8>> = base_rdm;
// HOSTDEMO_KNOWN=uid12hex[,uid12hex...] 覆盖默认灯具清单
// (用于对测仿真 Butler/其他 UID 的设备; 默认 = 台架 3 台真灯)
let known: Vec<[u8; 6]> = match std::env::var("HOSTDEMO_KNOWN") {
Ok(v) if !v.trim().is_empty() => v
.split(',')
.filter_map(|s| {
let s = s.trim();
if s.len() != 12 { return None; }
let mut u = [0u8; 6];
for i in 0..6 {
u[i] = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok()?;
}
Some(u)
})
.collect(),
// SL 式流程不预置灯具地址: 未设 HOSTDEMO_KNOWN 时从空清单开始,
// 灯具完全经广播 UN_MUTE + UNIQUE_BRANCH 二分发现获得。
_ => vec![],
};
let start = Instant::now();
let mut di = 0usize;
let mut qseq: u16 = 0;
let mut last_rdm = Instant::now() - Duration::from_secs(10);
let mut step = 0usize;
let mut addr_list: Vec<u16> = Vec::new();
let mut resp_n = 0u64;
let mut state: Vec<([u8; 6], u16, u16)> = Vec::new();
let mut last_status = Instant::now() - Duration::from_secs(100);
// ★消融实证: 周期宣告/发现广播是 RDM 代理开关 (nobcast 失败, nodmx 成功)
let disc = load_template("discovery_req");
let mut ann = load_template("announce");
ann[0x41..0x45].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
let mut last_bcast = Instant::now() - Duration::from_secs(10);
while secs == 0 || start.elapsed() < Duration::from_secs(secs) {
let t = start.elapsed().as_secs_f32();
if last_bcast.elapsed() >= Duration::from_millis(2500) {
let _ = sock.send_to(&disc, "255.255.255.255:31772");
let _ = sock.send_to(&ann, &format!("{}:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123").to_string() + ".255"));
last_bcast = Instant::now();
}
// verbatim DMX 循环
// DMX 数据替换
let mut p1 = [0u8; 512];
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 {
let wave = ((t * 0.6 + i as f32 * 0.55).sin() * 0.5 + 0.5);
p1[i] = (wave * breathe * 255.0) as u8;
}
} else if std::env::var("SHOW_PATTERN").unwrap_or_default() == "1" {
for (k, &a) in [101u16, 123, 1].iter().enumerate() {
let phase = t * 1.0 + k as f32 * 2.1;
let v = (((phase.sin() * 0.5 + 0.5) * 220.0) * (t * 0.3).min(1.0)) as u8;
p1[(a - 1) as usize] = v;
}
}
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, &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());
fx[0x22..0x26].copy_from_slice(&rr1.to_le_bytes());
fx[0x26..0x2A].copy_from_slice(&rr2.to_le_bytes());
fx[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, rr1, bc));
fx[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], rr2, bc));
fx
};
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 pd: Vec<u8> = Vec::new();
let mut pid: u16 = 0x0001;
let mut dest = [0xffu8; 6];
let mut cc = 0x10u8;
match step {
0 => { pid = 0x0003; }
s if s <= known.len() => {
let v = uid_u64(&known[s - 1]);
pd.extend_from_slice(&u64_uid(v - 1));
pd.extend_from_slice(&u64_uid(v + 1));
}
_ => {
let gi = step - known.len() - 1; // GET 阶段索引
if gi < known.len() { dest = known[gi]; pid = 0x0060; cc = 0x20; }
else { step = known.len(); } // 回到首个 GET 步循环
}
}
let tn = (step * 7 + 1) as u8;
let (f, f2) = if is_show {
// ★纯自构造 RDM 帧 (零模板) + 成对副本
qseq = qseq.wrapping_add(1);
let f = build_rdm_frame2(&dest, tn, qseq, cc, pid, &pd);
let f2 = rdm_pair_copy(&f);
(f, f2)
} else {
let mut f = rdm_t.clone().unwrap();
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] = tn;
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);
(f, f2)
};
let _ = sock.send_to(&f, &dst);
let _ = sock.send_to(&f2, &dst);
println!("[{}] RDM step={} pid={:04x}", label, 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_show {
// show 模式维护状态表
if is_pulse {
if let Some(uid) = decode_pulse(d) {
println!(" [{}] ★脉冲 UID={}", label, uid_s(&uid));
}
} else if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
let uid = [d[9], d[10], d[11], d[12], d[13], d[14]];
let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]);
let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]);
if let Some(e) = state.iter_mut().find(|(u, _, _)| *u == uid) {
if e.1 != a || e.2 != fp {
println!(" [{}] ★{} 状态变更: 地址 {}→{} 通道 {}→{}", label, uid_s(&uid), e.1, a, e.2, fp);
e.1 = a; e.2 = fp;
}
} else {
state.push((uid, a, fp));
println!(" [{}] ★设备注册: {} 地址={} 通道={} 传感器={}", label, uid_s(&uid), a, fp, d[0x18 + 18]);
}
} else if d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0xF0] {
let uid = [d[9], d[10], d[11], d[12], d[13], d[14]];
let a = u16::from_be_bytes([d[0x18], d[0x19]]);
if let Some(e) = state.iter_mut().find(|(u, _, _)| *u == uid) {
if e.1 != a {
println!(" [{}] ★{} START_ADDRESS 变更: {}→{}", label, uid_s(&uid), e.1, a);
e.1 = a;
}
}
}
} else {
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());
}
}
}
}
// show 模式每 10s 打印状态汇总
if is_show && last_status.elapsed() >= Duration::from_secs(10) {
last_status = Instant::now();
let lines: Vec<String> = state.iter().map(|(u, a, fp)| format!("{} @DMX{} ({}ch)", uid_s(u), a, fp)).collect();
println!("[SHOW] 状态: {} 台 | {} | 收帧{}", state.len(), lines.join(" | "), resp_n);
}
std::thread::sleep(Duration::from_millis(33));
}
println!("[{}] 结束: 应答 {} 帧, 地址 {:?}", label, resp_n, if is_show { state.iter().map(|(_, a, _)| *a).collect::<Vec<_>>() } else { addr_list.clone() });
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<u8>)> = 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<u8> = 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::<Vec<_>>(), addr_map);
return;
}
// slbisect: SL 原帧逐字段突变 — 定位 Butler 拒收我们 RDM 帧的确切字段
// 突变序列: verbatim(对照) → counter → TN → qseq → 全部 → 纯我们构造
if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "slbisect" {
// 认领
let o2: Vec<u8> = 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!("[B] 认领完成 B12=0x{:08x}", bc);
// verbatim DMX 背景流 + 突变 branch 查询
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<u8>> = Vec::new();
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 byte = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap();
if byte(0x18) == 1 && byte(0x20) == 1 && byte(0x21) == 1 && byte(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);
}
}
let sl_branch = std::fs::read_to_string("C:/Users/tonycao/AppData/Local/Temp/opencode/sl_branch_exact.hex")
.unwrap_or_else(|_| String::new());
let mut slb: Vec<u8> = Vec::new();
{
let h = sl_branch.trim();
for i in (0..h.len()).step_by(2) { slb.push(u8::from_str_radix(&h[i..i + 2], 16).unwrap()); }
}
// NFS1K2D GET DEVICE_INFO 探针 (被 MUTE 的设备也须应答 GET — 应答可观测)
let sl_get = std::fs::read_to_string("C:/Users/tonycao/AppData/Local/Temp/opencode/sl_get_exact.hex")
.unwrap_or_default();
let mut slg: Vec<u8> = Vec::new();
{
let h = sl_get.trim();
for i in (0..h.len()).step_by(2) { slg.push(u8::from_str_radix(&h[i..i + 2], 16).unwrap()); }
}
let known: [[u8; 6]; 3] = [
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25],
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00],
[0x08, 0x99, 0x50, 0x00, 0x26, 0xc9],
];
println!("[B] 开始突变序列 (GET→NFS; 每步 1s DMX 背景 + 发送 + 500ms 侦听)");
let mut di = 0usize;
let mutations = ["verbatim对照", "counter=ours", "TN=ours", "qseq=ours", "全突变"];
for round in 0..2 {
for (mi, mname) in mutations.iter().enumerate() {
let seg = Instant::now();
while seg.elapsed() < Duration::from_millis(1000) {
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); }
std::thread::sleep(Duration::from_millis(33));
while sock.recv_from(&mut buf).is_ok() {}
}
let mut f = slg.clone();
let ctr = (di as u32).wrapping_add(9000);
match mi {
1 => { f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); }
2 => { f[0x4A] = 0x77; }
3 => { f[0x22] = 0x55; f[0x23] = 0x00; }
4 => {
f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes());
f[0x4A] = 0x78;
f[0x22] = 0x56; f[0x23] = 0x00;
}
_ => {}
}
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);
let mut info = false;
let mut any = 0usize;
let w = Instant::now();
// ★边发 DMX 边侦听 (Butler 仅在 DMX 流持续时吐应答, 停流即饿死)
while w.elapsed() < Duration::from_millis(500) {
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); }
while let Ok((n, src)) = sock.recv_from(&mut buf) {
if src.ip().to_string() != target { continue; }
let b = &buf[..n];
any += 1;
if let Some((false, d)) = rdm_region(b) {
if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
info = true;
}
}
}
std::thread::sleep(Duration::from_millis(33));
}
println!(" [B] r{} {:<14} → 收帧 {} DEVICE_INFO={}", round, mname, any, info);
}
}
let _ = known;
return;
}
// show: 地址 1-32 炫彩呼吸持续播放 + 并行 RDM 状态轮询 (DEVICE_INFO / DMX 地址)
// 旧的 show 实现已合并到上方 hybrid/show 块;保留结构,条件永假
if false {
let o2: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
let bcast_subnet = format!("{}:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123").to_string() + ".255");
// ---- 冷启动前奏 (默认模式同款; RDM 代理门控, 短前奏 RDM 无应答) ----
let pre_secs: u64 = std::env::var("HOSTDEMO_PRE").ok().and_then(|s| s.parse().ok()).unwrap_or(30);
let disc = load_template("discovery_req");
let euce = load_template("euce");
let pre = Instant::now();
let mut last = Instant::now() - Duration::from_secs(10);
let mut last_euce = Instant::now() - Duration::from_secs(10);
while pre.elapsed() < Duration::from_secs(pre_secs) {
if last.elapsed() >= Duration::from_secs(2) {
let _ = sock.send_to(&disc, "255.255.255.255:31772");
let mut ann = build_fresh_announce(&mut rng);
ann[0x41..0x45].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
let _ = sock.send_to(&ann, &bcast_subnet);
last = Instant::now();
}
if last_euce.elapsed() >= Duration::from_secs(6) {
let _ = sock.send_to(&euce, "255.255.255.255:31772");
last_euce = Instant::now();
}
if let Ok((n, src)) = sock.recv_from(&mut buf) {
println!(" [SHOW/前奏] 收帧 len={} src={}", n, src);
}
}
for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] {
let mut b = build_bind52(flag);
b[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
sock.send_to(&b, &dst).unwrap();
std::thread::sleep(Duration::from_millis(320));
}
std::thread::sleep(Duration::from_millis(1800));
let mut ident = IDENT102.to_vec();
ident[0x1C..0x20].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
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(400));
println!("[SHOW] 认领完成 B12=0x{:08x}; 地址1-32 炫彩呼吸 + RDM 状态轮询 (Ctrl+C 停止)", bc);
let start = Instant::now();
// SL 实帧 RDM 模板 (自构造帧 Butler 不响应, SL 帧响应 — 实测结论)
let sl_rdm_tpl: Option<Vec<u8>> = std::fs::read_to_string("C:/Users/tonycao/AppData/Local/Temp/opencode/sl_get_exact.hex").ok().map(|h| {
let h = h.trim();
let mut v = Vec::new();
for i in (0..h.len()).step_by(2) { v.push(u8::from_str_radix(&h[i..i + 2], 16).unwrap()); }
v
});
let disc = load_template("discovery_req");
let mut ann = load_template("announce");
ann[0x41..0x45].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
let known: [[u8; 6]; 3] = [
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25],
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00],
[0x08, 0x99, 0x50, 0x00, 0x26, 0xc9],
];
// 状态表: UID → (DMX地址, 通道数)
let mut state: Vec<([u8; 6], u16, u16)> = Vec::new();
let mut di = 0usize;
let mut last_bcast = Instant::now() - Duration::from_secs(10);
let mut last_hb = Instant::now() - Duration::from_secs(10);
let mut last_rdm = Instant::now() - Duration::from_secs(10);
let mut poll_idx = 0usize;
let mut addr_req_pending = false;
let mut last_status = Instant::now() - Duration::from_secs(100);
let mut resp_n: u64 = 0;
loop {
// 广播 (RDM 代理开关)
if last_bcast.elapsed() >= Duration::from_millis(2500) {
let _ = sock.send_to(&disc, "255.255.255.255:31772");
let _ = sock.send_to(&ann, &format!("{}:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123").to_string() + ".255"));
last_bcast = Instant::now();
}
// 心跳
if last_hb.elapsed() >= Duration::from_secs(1) {
let _ = sock.send_to(&load_template("heartbeat"), &dst);
last_hb = Instant::now();
}
// RDM 轮询: 每 700ms 一步, 轮转 3 台 × (DEVICE_INFO / START_ADDRESS)
if last_rdm.elapsed() >= Duration::from_millis(700) {
last_rdm = Instant::now();
let uid = known[poll_idx % 3];
let (pid, cc) = if addr_req_pending { (0x00F0u16, 0x20u8) } else { (0x0060, 0x20) };
// RDM 请求用 SL 实帧模板 (实测: 自构造帧 Butler 不响应, SL 帧响应)
let mut f = match &sl_rdm_tpl {
Some(t) => {
let mut t = t.clone();
t[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());
t[0x3D] = 24; // msgLen (PDL=0)
t[0x3E..0x44].copy_from_slice(&uid);
t[0x4A] = (poll_idx * 13 + 1) as u8;
t[0x4F] = cc;
t[0x50] = (pid >> 8) as u8;
t[0x51] = (pid & 0xFF) as u8;
t[0x52] = 0;
let mut sum: u16 = 0xCC;
for i in 0x3C..0x3C + 24 { sum = sum.wrapping_add(t[i] as u16); }
t[0x5F] = (sum >> 8) as u8;
t[0x60] = (sum & 0xFF) as u8;
t
}
None => build_rdm_frame(&uid, (poll_idx * 13 + 1) as u8, poll_idx as u16, cc, pid, &[]),
};
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);
poll_idx += 1;
if poll_idx % 3 == 0 { addr_req_pending = !addr_req_pending; }
}
// 收包: 解析 DEVICE_INFO / START_ADDRESS, 报告变更
while let Ok((n, src)) = sock.recv_from(&mut buf) {
if src.ip().to_string() != target { continue; }
let b = &buf[..n];
if let Some((false, d)) = rdm_region(b) {
if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
let uid = [d[9], d[10], d[11], d[12], d[13], d[14]];
let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]);
let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]);
match state.iter_mut().find(|(u, _, _)| *u == uid) {
Some(e) => {
if e.1 != a || e.2 != fp {
println!(" [SHOW] ★{} 状态变更: 地址 {}→{} 通道 {}→{}", uid_s(&uid), e.1, a, e.2, fp);
e.1 = a; e.2 = fp;
}
}
None => {
state.push((uid, a, fp));
println!(" [SHOW] ★设备注册: {} 地址={} 通道={} 传感器={}", uid_s(&uid), a, fp, d[0x18 + 18]);
}
}
} else if d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0xF0] {
let uid = [d[9], d[10], d[11], d[12], d[13], d[14]];
let a = u16::from_be_bytes([d[0x18], d[0x19]]);
if let Some(e) = state.iter_mut().find(|(u, _, _)| *u == uid) {
if e.1 != a {
println!(" [SHOW] ★{} START_ADDRESS 变更: {}→{}", uid_s(&uid), e.1, a);
e.1 = a;
}
}
}
}
}
// 每 10s 打印一次状态汇总
if last_status.elapsed() >= Duration::from_secs(10) {
last_status = Instant::now();
let lines: Vec<String> = state.iter().map(|(u, a, fp)| format!("{} @DMX{} ({}ch)", uid_s(u), a, fp)).collect();
println!("[SHOW] 状态: {} 台 | {} | 收帧{}", state.len(), lines.join(" | "), resp_n);
}
// DMX @30fps: 地址 1-32 彩虹波 × 呼吸包络
let t = start.elapsed().as_secs_f32();
di += 1;
let mut p1 = [0u8; 512];
let breathe = (t * 0.9).sin() * 0.5 + 0.5; // 全局呼吸包络 0..1
for i in 0..32usize {
let wave = ((t * 1.2 + i as f32 * 0.55).sin() * 0.5 + 0.5); // 行波
p1[i] = (wave * breathe * 255.0) as u8;
}
let rr1 = rng.next();
let rr2 = rng.next();
let mut f = load_template("dmx_a");
f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());
f[0x22..0x26].copy_from_slice(&rr1.to_le_bytes());
f[0x26..0x2A].copy_from_slice(&rr2.to_le_bytes());
f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, rr1, bc));
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], rr2, bc));
let _ = sock.send_to(&f, &dst);
std::thread::sleep(Duration::from_millis(33));
}
}
let mut b12 = rng.next();
let mut claim_ok = false;
// claimonly/quick: 最小认领 (announce144 → 47B → AECH) — 实测成功配方, 无绑定/宣告/前奏
let hd_mode = std::env::var("HOSTDEMO_MODE").unwrap_or_default();
if hd_mode.starts_with("claim") || hd_mode == "quick" {
let o2: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
let full = hd_mode == "claimfull";
if full {
// sub=07 绑定帧 ×4 (ffff→0000→ffff→0000, 0.32s 间隔)
for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] {
let mut b = build_bind52(flag);
b[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
sock.send_to(&b, &dst).unwrap();
std::thread::sleep(Duration::from_millis(320));
}
std::thread::sleep(Duration::from_millis(1500));
}
let mut ident = IDENT102.to_vec();
ident[0x1C..0x20].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
// runs1-3 成功配方: 仅 announce144 (102B 身份帧) 单播一次, 无绑定/EUCE/宣告
let ident144 = if full { ident } else { load_template("announce144") };
sock.send_to(&ident144, &dst).unwrap();
let mut reg = false;
let w = Instant::now();
while w.elapsed() < Duration::from_millis(400) {
if let Ok((n, src)) = sock.recv_from(&mut buf) {
if src.ip().to_string() == target && n == 47 { reg = true; break; }
}
}
// CLAIM_BINDFF=1: 认领前先发单个 bind52(ffff), 测试能否解除 Butler 拒答态 (bind 中毒恢复实验)
if std::env::var("CLAIM_BINDFF").unwrap_or_default() == "1" {
let mut b = build_bind52([0xff, 0xff]);
b[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
sock.send_to(&b, &dst).unwrap();
std::thread::sleep(Duration::from_millis(320));
println!("CLAIM_BINDFF: 已发 bind52(ffff)");
}
let nonce = rng.next();
let b12c = 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(&b12c.to_le_bytes());
// CLAIM_ZERO=1: 清零不透明区 (0x2C-0x87, 0x90-0x123), 消融验证 Butler 是否校验
if std::env::var("CLAIM_ZERO").unwrap_or_default() == "1" {
for i in 0x2C..0x88 { ch[i] = 0; }
for i in 0x90..0x124 { ch[i] = 0; }
println!("CLAIM_ZERO: 不透明区已清零 (240B)");
}
sock.send_to(&ch, &dst).unwrap();
let w2 = Instant::now();
while w2.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(|w| w == b"AERE") {
let resp = g32(b, 0xC0);
println!("CLAIM_PROBE reg89={} AERE resp=0x{:08x} G=0x{:08x} {}", reg, resp, agios_g(nonce),
if resp == agios_g(nonce) { "PASS" } else { "FAIL" });
if resp == agios_g(nonce) {
// quick 模式: 认领成功后直通 RDM+DMX 阶段 (跳过绑定/宣告/前奏 — 实测有毒)
if hd_mode == "quick" {
b12 = b12c;
claim_ok = true;
break;
}
}
if !claim_ok { return; }
}
}
}
if !claim_ok {
println!("CLAIM_PROBE reg89={} AERE=none FAIL", reg);
return;
}
}
// takeover: 跳过认领, 用 SL 上次会话的 B12 续发 DMX+心跳, 探会话是否可直接延续
if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "takeover" {
let b12_old: u32 = 0x4366937a; // sym_startup @101.278 AECH 的 B12@0x8C (SL 22:52 会话)
println!("[P] takeover: B12=0x{:08x} 续发 15s DMX(ch101 ramp)+心跳+RDM GET, 侦听回包", b12_old);
let tpl = load_template("dmx_a");
let hb = load_template("heartbeat");
let known: [u8; 6] = [0x01, 0x07, 0x34, 0x67, 0x01, 0x25]; // NFS1K2D
let start = Instant::now();
let mut seq: u32 = 5000; // 帧序号跳开, 避免与 SL 旧计数冲突
let mut last_hb = Instant::now() - Duration::from_secs(10);
let mut rdm_done = false;
while start.elapsed() < Duration::from_secs(15) {
if last_hb.elapsed() >= Duration::from_secs(6) {
let _ = sock.send_to(&hb, &dst);
last_hb = Instant::now();
println!(" [P] 心跳已发");
}
if !rdm_done && start.elapsed() > Duration::from_secs(5) {
rdm_done = true;
let f = build_rdm_frame(&known, 1, 1, 0x20, 0x0060, &[]); // GET DEVICE_INFO
sock.send_to(&f, &dst).unwrap();
println!(" [P] RDM GET DEVICE_INFO 已发");
}
let t = start.elapsed().as_secs_f32();
let mut p1 = [0u8; 512];
p1[100] = (((t * 0.8).sin() * 0.5 + 0.5) * 255.0) as u8;
let p2 = [0u8; 512];
let r1 = rng.next();
let r2 = rng.next();
let mut f = tpl.clone();
f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes());
f[0x2E] = 2; f[0x2F] = 0;
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, b12_old));
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&p2, r2, b12_old));
let _ = sock.send_to(&f, &dst);
seq += 1;
while let Ok((n, src)) = sock.recv_from(&mut buf) {
let b = &buf[..n];
println!(" [P] ← len={} src={} head={}", n, src, hexs(&b[..std::cmp::min(16, n)]));
}
std::thread::sleep(Duration::from_millis(33));
}
return;
}
// ---- 2) 宣告: 真主机用 644B type=1 广播宣告 (announce 模板), 主机 IP@0x41 LE;
// 再发 102B type=2 身份帧 (cold2 实测: 紧邻 AECH 之前), IP@0x1C LE ----
let mode = std::env::var("HOSTDEMO_MODE").unwrap_or_default();
let o: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
let bcast = format!("{}.255:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123"));
if mode != "skip2" && mode != "noclaim" {
// 冷启动广播前奏 (cold2=38s, SymphoLight 实测 2.5min 成功; 10s 短前奏被拒!)
// ★门控假说: Butler 须收到 ~13+ 轮 discovery/announce 才武装认领应答
// ★2026-09-11 消融证伪: 广播前奏不仅非必需而且有毒 (PRE=45 认领FAIL / PRE=0 PASS)
// → 默认 PRE=0; 复现 SL 行为用 HOSTDEMO_PRE=45
let pre_secs: u64 = std::env::var("HOSTDEMO_PRE").ok().and_then(|s| s.parse().ok()).unwrap_or(0);
// ★宣告帧用 announce_live 模板 + 新鲜时间戳/随机会话 ID (陈旧宣告 = 僵尸主机, 认领被无视)
let euce = load_template("euce"); // 540B, 无主机相关字段
let pre = Instant::now();
let mut queries_dbg = 0;
let mut last = Instant::now() - Duration::from_secs(10);
let mut last_euce = Instant::now() - Duration::from_secs(10);
while pre.elapsed() < Duration::from_secs(pre_secs) {
if last.elapsed() >= Duration::from_secs(2) {
let _ = sock.send_to(&disc, "255.255.255.255:31772");
let mut ann = build_fresh_announce(&mut rng);
ann[0x41..0x45].copy_from_slice(&[o[3], o[2], o[1], o[0]]); // 主机IP逆序字节@0x41
if queries_dbg == 0 {
queries_dbg = 1;
let live = load_template("announce_live");
println!(" [dbg] ann timestamp区: ours={}", hexs(&ann[0x230..0x245]));
println!(" [dbg] ann timestamp区: live={}", hexs(&live[0x230..0x245]));
println!(" [dbg] ann sessid区: ours={}", hexs(&ann[0x219..0x226]));
println!(" [dbg] ann sessid区: live={}", hexs(&live[0x219..0x226]));
let diffs: Vec<usize> = (0..ann.len()).filter(|&i| ann[i] != live[i]).collect();
println!(" [dbg] ann 与 live 模板差异偏移 ({}处): {:?}", diffs.len(), diffs);
}
let _ = sock.send_to(&ann, &bcast);
last = Instant::now();
}
if last_euce.elapsed() >= Duration::from_secs(6) {
// ★EUCE 每 6s (SymphoLight 启动期实测节奏, 30s 节奏认领被拒)
let _ = sock.send_to(&euce, "255.255.255.255:31772");
last_euce = Instant::now();
}
// 排空 (217B 发现应答 / 47B 注册等)
if let Ok((n, src)) = sock.recv_from(&mut buf) {
println!(" [dbg] 前奏期收帧 len={} src={}", n, src);
}
}
// ★bind52×4 实测有毒 (2026-09-11 消融: skip2 认领PASS / 仅加bind×4 即 FAIL) —
// ffff→0000→ffff→0000 以 0000(unbind) 收尾, Butler 进入未绑定态后无视 AECH。
// 默认不再发送; 复现实验用 HOSTDEMO_BIND=1 开启。
if std::env::var("HOSTDEMO_BIND").unwrap_or_default() == "1" {
for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] {
let mut b = build_bind52(flag);
b[0x1A..0x1E].copy_from_slice(&[o[3], o[2], o[1], o[0]]);
sock.send_to(&b, &dst).unwrap();
std::thread::sleep(Duration::from_millis(320));
}
std::thread::sleep(Duration::from_millis(1800));
}
// IDENT102 常量帧 0x1A 起已是本机 IP+MAC — 不要再补丁 (旧代码 patch 0x1C 会踩坏 MAC 高2字节!)
sock.send_to(&IDENT102, &dst).unwrap();
println!("[2] 冷启动前奏完成 ({}s 广播 + 身份帧; bind 已默认跳过=有毒)", pre_secs);
}
let mut reg_ok = false;
for _ in 0..10 {
if let Ok((n, _)) = sock.recv_from(&mut buf) {
if n == 89 { reg_ok = true; break; }
}
}
println!("[2] 宣告 → 注册应答: {}", if reg_ok { "OK (89B)" } else { "未收到 (继续)" });
// ---- 3) AECH 认领 ×3 (quick 模式已在上面认领成功; noclaim 跳过) ----
if !claim_ok {
b12 = rng.next();
if mode == "noclaim" {
println!("[3] noclaim 模式: 跳过 AECH, 直接以 RDM GET 探测连接态");
claim_ok = true;
}
for round in 0..3 {
if mode == "noclaim" { break; }
// ★每轮先发身份帧(announce144)等 47B 注册应答, 再发认领 — skip2 实测: 无身份帧直接 claim 被无视
let o2a = [o[0], o[1], o[2], o[3]];
sock.send_to(&load_template("announce144"), &dst).unwrap();
let wr = Instant::now();
let mut reg47 = false;
while wr.elapsed() < Duration::from_millis(400) {
if let Ok((n, _)) = sock.recv_from(&mut buf) {
if n == 47 { reg47 = true; break; }
}
}
let nonce = rng.next();
let ch = build_claim_frame(o2a, nonce, b12);
// 排空缓存 (前奏自广播回收), 避免 AERE 到达前读循环耗尽
while sock.recv_from(&mut buf).is_ok() {}
sock.send_to(&ch, &dst).unwrap();
let w = Instant::now();
let mut rx_n = 0u32;
let mut rx_lens: Vec<usize> = Vec::new();
while w.elapsed() < Duration::from_millis(700) {
if let Ok((n, _)) = sock.recv_from(&mut buf) {
rx_n += 1;
if rx_lens.len() < 8 { rx_lens.push(n); }
let b = &buf[..n];
if n > 0xC4 && b.windows(4).any(|w| w == b"AERE") {
let resp = g32(b, 0xC0);
let want = agios_g(nonce);
println!("[3] AECH 第{}轮: reg47={} resp=0x{:08x} G=0x{:08x} {}", round + 1, reg47, resp, want,
if resp == want { "★PASS" } else { "FAIL" });
if resp == want { claim_ok = true; }
break;
}
}
}
if !claim_ok { println!("[3] AECH 第{}轮: reg47={} 无AERE (等700ms内收{}帧 {:?})", round + 1, reg47, rx_n, rx_lens); }
}
}
if !claim_ok { println!("[3] 认领失败 (真机只应答源 123.113 的 AECH), 终止"); return; }
println!(" 认领通过, B12=0x{:08x}", b12);
// quick 模式: 密钥广播 + 统一会话环 — DMX 持续流 (cold2 实证: RDM 代理仅在 DMX 流活跃时工作)
if hd_mode == "quick" {
println!("[K] 1947 许可密钥广播 4s...");
send_key_burst(&sock);
session_loop(&sock, &dst, target, &mut rng, b12, secs);
return;
}
// noclaim 连接态探测: 直接 GET DEVICE_INFO 已知 3 灯具, 验证 Butler 是否仍视我们为主机
if mode == "noclaim" {
let known: [[u8; 6]; 3] = [
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2D DMX101
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680S DMX112
[0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32
];
let mut tn0: u8 = rng.byte();
for (i, uid) in known.iter().enumerate() {
tn0 = tn0.wrapping_add(1);
let f = build_rdm_frame(uid, tn0, (i + 1) as u16, 0x20, 0x0060, &[]);
sock.send_to(&f, &dst).unwrap();
let w = Instant::now();
let mut got = false;
while w.elapsed() < Duration::from_millis(500) {
if let Ok((n, src)) = sock.recv_from(&mut buf) {
if src.ip().to_string() != target { continue; }
let b = &buf[..n];
println!(" [dbg] 收帧 len={} 0x18={:02x}{:02x}", n, b.get(0x19).copied().unwrap_or(0), b.get(0x18).copied().unwrap_or(0));
if let Some((false, d)) = rdm_region(b) {
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]]);
println!(" ★ 连接态确认! UID={} DMX地址={}", uid_s(uid), a);
got = true;
break;
}
}
}
}
if !got { println!(" UID={} 无应答", uid_s(uid)); }
}
return;
}
// ---- 4) RDM 发现 ----
let mut tn: u8 = rng.byte();
let mut qseq: u16 = 1;
let mut fixtures: Vec<[u8; 6]> = Vec::new();
{
// SL 式广播发现流程: 不预知灯具地址, 全部经 UN_MUTE 广播 +
// UNIQUE_BRANCH 二分发现 + 逐台 MUTE 收敛 (见下)。
// 先广播 DISC_UN_MUTE (PID=0003): 稳态下主机已把全部灯具 MUTE,
// 被 MUTE 的灯具不应答 UNIQUE_BRANCH → 须先解除静音才能重新发现
tn = tn.wrapping_add(1);
let um = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0003, &[]);
qseq += 1;
sock.send_to(&um, &dst).unwrap();
std::thread::sleep(Duration::from_millis(80));
println!("[4] 已广播 DISC_UN_MUTE, 开始 UNIQUE_BRANCH 二分遍历");
let mut stack: Vec<(u64, u64)> = vec![(0, 0xFFFF_FFFF_FFFF)];
let mut queries = 0;
let walk_start = Instant::now();
while let Some((lo, hi)) = stack.pop() {
if queries >= 400 || walk_start.elapsed() > Duration::from_secs(120) { println!(" (发现遍历到达上限)"); break; }
let mut pd = Vec::with_capacity(12);
pd.extend_from_slice(&u64_uid(lo));
pd.extend_from_slice(&u64_uid(hi));
// 真机实测: 首个应答延迟可达 ~1.4s, 真主机对同一分支连发多次 → 每分支最多 4 次尝试 × 250ms 窗口
let mut found: Vec<[u8; 6]> = Vec::new();
let mut collision = false;
for attempt in 0..4 {
queries += 1;
tn = tn.wrapping_add(1);
let f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0001, &pd); // DISC_UNIQUE_BRANCH
qseq += 1;
if queries == 1 {
let t = load_template("rdm_disc_req");
let diffs: Vec<usize> = (0x2A..0x62).filter(|&i| f[i] != t[i]).collect();
println!(" [dbg] 首帧与模板差异偏移: {:?} (应仅 qseq/TN/srcUID末字节/PD/checksum)", diffs);
}
sock.send_to(&f, &dst).unwrap();
let w = Instant::now();
while w.elapsed() < Duration::from_millis(250) {
if let Ok((n, src)) = sock.recv_from(&mut buf) {
let b = &buf[..n];
if src.ip().to_string() != target { continue; }
if let Some((is_pulse, d)) = rdm_region(b) {
if is_pulse {
match decode_pulse(d) {
Some(uid) => { if !found.contains(&uid) { found.push(uid); } }
None => collision = true,
}
}
}
}
}
if !found.is_empty() || collision { break; }
let _ = attempt;
}
if collision {
let mid = lo + (hi - lo) / 2;
stack.push((mid + 1, hi));
stack.push((lo, mid));
} else if found.len() == 1 {
let uid = found[0];
if found_valid(&uid) {
println!(" [发现] UID={} (深度 {} 次查询后)", uid_s(&uid), queries);
// DISC_MUTE (PID=0002) → 70B 参数 ACK
tn = tn.wrapping_add(1);
let f2 = build_rdm_frame(&uid, tn, qseq, 0x10, 0x0002, &[]);
qseq += 1;
sock.send_to(&f2, &dst).unwrap();
let w2 = Instant::now();
let mut acked = false;
while w2.elapsed() < Duration::from_millis(400) {
if let Ok((n, src)) = sock.recv_from(&mut buf) {
if src.ip().to_string() != target { continue; }
let b = &buf[..n];
if let Some((false, d)) = rdm_region(b) {
if d.len() >= 0x18 && d[0] == 0xCC && d[0x14] == 0x11 && d[0x15..0x17] == [0x00, 0x02] {
acked = true; break;
}
}
}
}
println!(" MUTE(0002) ACK: {}", if acked { "OK" } else { "未收到" });
fixtures.push(uid);
}
} else if found.len() > 1 {
let mid = lo + (hi - lo) / 2;
stack.push((mid + 1, hi));
stack.push((lo, mid));
}
// found==0: 空分支
}
println!("[4] RDM 发现完成: {} 台灯具, {} 次查询, {:.1}s", fixtures.len(), queries, walk_start.elapsed().as_secs_f32());
}
// ---- 4b) 读参数: DEVICE_INFO / DMX_START_ADDRESS / DEVICE_LABEL ----
let mut addr_list: Vec<u16> = Vec::new();
for uid in &fixtures {
tn = tn.wrapping_add(1);
let f = build_rdm_frame(uid, tn, qseq, 0x20, 0x0060, &[]); // GET DEVICE_INFO
qseq += 1;
sock.send_to(&f, &dst).unwrap();
let mut dmx_addr: Option<u16> = None;
let mut footprint = 0u16;
let mut sensors = 0u8;
let w = Instant::now();
while w.elapsed() < Duration::from_millis(400) {
if let Ok((n, src)) = sock.recv_from(&mut buf) {
if src.ip().to_string() != target { continue; }
let b = &buf[..n];
if let Some((false, d)) = rdm_region(b) {
// d: CC@0 ... CC字段@0x14, PID@0x15-16, PDL@0x17, PD@0x18
if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x14] == 0x21 && d[0x15..0x17] == [0x00, 0x60] {
let pd = &d[0x18..0x18 + 19];
footprint = u16::from_be_bytes([pd[10], pd[11]]);
dmx_addr = Some(u16::from_be_bytes([pd[14], pd[15]]));
sensors = pd[18];
break;
}
}
}
}
// GET DMX_START_ADDRESS 复核
tn = tn.wrapping_add(1);
let f2 = build_rdm_frame(uid, tn, qseq, 0x20, 0x00F0, &[]);
qseq += 1;
sock.send_to(&f2, &dst).unwrap();
let w2 = Instant::now();
while w2.elapsed() < Duration::from_millis(400) {
if let Ok((n, src)) = sock.recv_from(&mut buf) {
if src.ip().to_string() != target { continue; }
let b = &buf[..n];
if let Some((false, d)) = rdm_region(b) {
if d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x14] == 0x21 && d[0x15..0x17] == [0x00, 0xF0] {
let a = u16::from_be_bytes([d[0x18], d[0x19]]);
if dmx_addr.is_none() { dmx_addr = Some(a); }
break;
}
}
}
}
match dmx_addr {
Some(a) => {
println!(" 灯具 {} → DMX 地址 {} (占用 {} 通道, {} 传感器)", uid_s(uid), a, footprint, sensors);
addr_list.push(a);
}
None => println!(" 灯具 {} → 参数读取超时", uid_s(uid)),
}
}
// ---- 5) DMX 控制: 呼吸图案 (按发现的灯具地址; 无发现则用已知 101/112/1) ----
if secs == 0 { println!("[5] secs=0, 跳过 DMX 控制"); return; }
if addr_list.is_empty() { addr_list = vec![101, 112, 1]; println!("[5] 无发现结果, 用已知地址 101/112/1"); }
println!("[5] DMX 控制 {}s @30fps: 呼吸图案 @地址 {:?} (zPprv0TXRRW 加密, B12=0x{:08x})", secs, addr_list, b12);
println!(" ⚠ SymphoLight 若同时运行会插黑场帧造成闪烁; 关其输出可得纯净效果");
let tpl = load_template("dmx_a");
let hb = load_template("heartbeat"); // 36B type=02 sub=03 心跳 (会话保活, 每 ~6s 单播)
let start = Instant::now();
let mut seq: u32 = 1;
let mut sent = 0u64;
let mut last_hb = Instant::now();
while start.elapsed() < Duration::from_secs(secs) {
if last_hb.elapsed() >= Duration::from_secs(6) {
let _ = sock.send_to(&hb, &dst);
last_hb = Instant::now();
}
let t = start.elapsed().as_secs_f32();
let mut p1 = [0u8; 512];
let mut p2 = [0u8; 512];
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) as u8;
let ch = a.max(1) - 1; // 0-based
if ch < 512 { p1[ch as usize] = v; } else if ch < 1024 { p2[(ch - 512) as usize] = v; }
}
let r1 = rng.next();
let r2 = rng.next();
let mut f = tpl.clone();
f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes());
f[0x2E] = 2; f[0x2F] = 0; // 节点序号 (真机 .1 = 2)
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, b12));
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&p2, r2, b12));
if sock.send_to(&f, &dst).is_ok() { sent += 1; }
seq += 1;
std::thread::sleep(Duration::from_millis(33));
}
println!("[5] 完成: 发送 {} 帧", sent);
}
fn found_valid(uid: &[u8; 6]) -> bool {
uid.iter().any(|&b| b != 0) && uid.iter().any(|&b| b != 0xFF)
}
/// 构造"新鲜"宣告帧: announce_live 模板 (今日 SL 实抓) + 每发随机会话 ID@0x221
/// + 当前本地时间戳@0x239 (年u16LE 月 日 时 分 秒 毫秒u16LE 时区) — Butler 按
/// 宣告时间戳判主机新鲜度, 陈旧宣告(旧抓包模板)会被当僵尸主机无视 ident/AECH
fn build_fresh_announce(rng: &mut Rng) -> Vec<u8> {
let mut a = load_template("announce_live"); // 644B
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
// 本地时间分解 (UTC+8, 与实测 tz=02... 用本地时区偏移)
let secs = now + tz_offset_secs();
let days = secs / 86400;
let rem = secs % 86400;
let (year, month, day) = civil_from_days(days as i64);
let hour = (rem / 3600) as u8;
let min = ((rem % 3600) / 60) as u8;
let sec = (rem % 60) as u8;
let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_millis();
// 时间戳字段实际位于 0x235: 年u16LE@0x235 月@0x237 日@0x238 时@0x239 分@0x23A
// 秒@0x23B 毫秒u16LE@0x23C 时区@0x23E (实测 2026-09-10 22:52:50.992 tz=02)
a[0x235] = (year & 0xFF) as u8;
a[0x236] = (year >> 8) as u8;
a[0x237] = month;
a[0x238] = day;
a[0x239] = hour;
a[0x23A] = min;
a[0x23B] = sec;
let msle = (ms as u16).to_le_bytes();
a[0x23C] = msle[0];
a[0x23D] = msle[1];
a[0x23E] = 0x02; // 时区字段 (实测值)
// 4B 随机会话 ID @0x21D (每发宣告都变; 0x221 的 0c 恒定)
let id = rng.next();
a[0x21D..0x221].copy_from_slice(&id.to_le_bytes());
a
}
fn tz_offset_secs() -> u64 {
// 本地时区偏移 (秒): 用 UTC 与本地时间差
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
let utc_days = (now.div_euclid(86400) + 4) % 7; // 0=周四
let _ = utc_days;
// 简化: 读环境 TZ 不可靠, 直接用 Windows 时区 API? 用 chrono 不行(无依赖).
// 实测帧 tz 字节恒 02, 时间字段为本地时间 (UTC+8 中 hour=22 对应当地 22 点)
// → 这里返回 +8h; 若部署在其它时区需调整
8 * 3600
}
fn civil_from_days(z: i64) -> (u16, u8, u8) {
// Howard Hinnant 算法: days since 1970-01-01 → (y, m, d)
let z = z + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u8;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u8;
(if m <= 2 { (y + 1) as u16 } else { y as u16 }, m, d)
}
/// ★1947 端口"密钥报文" (allports.pcap 实测): 40B ASCII 许可令牌, SL 每 ~34s 广播一轮
/// (双目标 21 帧 @200ms) — Butler 在 1947 监听, 疑为 RDM 代理的许可开关
const KEY40: &[u8] = b"BlZRQphAGxUbi7at8KEfmdZ7TdSB03b+qoC1RIA";
fn send_key_burst(sock: &UdpSocket) {
for _ in 0..21 {
let _ = sock.send_to(KEY40, "192.168.123.255:1947");
let _ = sock.send_to(KEY40, "255.255.255.255:1947");
std::thread::sleep(Duration::from_millis(200));
}
}
/// ★AECH 认领质询帧 (552B) 从零构造 — 2026-09-11 全字段破解:
/// 不透明区 A(0x2C-0x87)/B(0x90-0x123) Butler 不校验 (CLAIM_ZERO 消融实证: 清零后 AERE 仍 PASS), 填 0
fn build_claim_frame(host_ip: [u8; 4], nonce: u32, b12: u32) -> Vec<u8> {
let mut f = Vec::with_capacity(552);
f.extend_from_slice(b"ENet");
f.extend_from_slice(&2u32.to_le_bytes()); // type=2
f.extend_from_slice(&[0x00, 0x00, 0x1c, 0x7c]); // 目标端口 31772
f.extend_from_slice(&[0u8; 4]);
f.extend_from_slice(&[0x00, 0x00, 0x01, 0x00]);
f.extend_from_slice(&552u32.to_le_bytes()); // 0x14 len
f.extend_from_slice(&[0x05, 0x00]); // 0x18 sub=05
f.extend_from_slice(&[host_ip[3], host_ip[2], host_ip[1], host_ip[0]]); // 0x1A 主机IP 逆序
f.extend_from_slice(&[0x30, 0x0e, 0xd5, 0x59, 0xbb, 0x36]); // 0x1E 主机MAC (本机)
f.extend_from_slice(&[0u8; 4]); // 0x24
f.extend_from_slice(b"AECH"); // 0x28
f.extend_from_slice(&[0u8; 0x88 - 0x2C]); // 0x2C-0x87 不透明区A = 0
f.extend_from_slice(&nonce.to_le_bytes()); // 0x88 nonce
f.extend_from_slice(&b12.to_le_bytes()); // 0x8C B12 (DMX rot 基值)
f.extend_from_slice(&[0u8; 552 - 0x90]); // 0x90-0x227 不透明区B+零填充
f
}
/// 统一会话环 (quick): DMX 30fps 持续 + RDM 穿插
/// 阶段: 0-2s 黑场(设备预热) → UN_MUTE + UNIQUE_BRANCH 二分发现 → MUTE 已发现灯具
/// → GET DEVICE_INFO/DMX 地址 → 呼吸图案控制 (未发现则用已知地址)
fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u32, secs: u64) {
let tpl = load_template("dmx_a");
let hb = load_template("heartbeat");
let known: [[u8; 6]; 3] = [
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2D DMX101
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680S DMX112
[0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32
];
let mut buf = [0u8; 4096];
let mut fixtures: Vec<[u8; 6]> = Vec::new();
let mut tn: u8 = rng.byte();
let mut qseq: u16 = 1;
let mut seq: u32 = 1;
// 发现任务队列: ★窄分支优先 (已知灯具各一个只含自己的分支 — 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));
}
/// 一台灯具的参数快照 (参数扫描汇总用)
struct DevInfo {
uid: [u8; 6],
addr: u16,
footprint: u16,
proto_major: u8,
proto_minor: u8,
model: u16,
sw: [u8; 4],
sensors: u16,
label: Option<String>,
model_desc: Option<String>,
sw_label: Option<String>,
manufacturer: Option<String>,
}
let mut get_queue: Vec<([u8; 6], u16)> = Vec::new();
let mut devinfo: Vec<DevInfo> = Vec::new();
let mut param_sweep_started = false;
let mut param_summary_done = false;
let mut sweep_round: u32 = 0;
let mut answered: Vec<([u8; 6], u16)> = Vec::new();
let mut last_sweep_rx = Instant::now();
let mut addr_by_uid: Vec<([u8; 6], u16)> = Vec::new();
let mut last_reply: Option<([u8; 6], u16, Instant)> = None;
let mut status_poll_count: u32 = 0;
let mut next_status_poll = Instant::now();
const STATUS_POLL_INTERVAL: Duration = Duration::from_secs(6);
let mut muted: Vec<[u8; 6]> = Vec::new();
let start = Instant::now();
let mut last_hb = Instant::now();
let mut last_key_t: f32 = -100.0;
let mut last_rdm = Instant::now() - Duration::from_secs(1);
let mut unmuting = false;
let mut discovering = true;
let mut collecting = false;
let mut collect_until = Instant::now();
let mut found: Vec<[u8; 6]> = Vec::new();
let mut collision = false;
let mut current_branch: Option<(u64, u64)> = None;
let mut sent = 0u64;
println!("[S] 统一会话环启动: DMX 30fps + RDM 穿插 ({}s)", secs);
while start.elapsed() < Duration::from_secs(secs) {
let t = start.elapsed().as_secs_f32();
// --- 心跳每 6s + 密钥每 30s + ★EUCE 每 6s (SL 全程 6s 节奏, 许可信标) ---
if last_hb.elapsed() >= Duration::from_secs(6) {
let _ = sock.send_to(&hb, dst);
let euce = load_template("euce");
let _ = sock.send_to(&euce, "255.255.255.255:31772");
last_hb = Instant::now();
}
if t - last_key_t >= 30.0 {
let _ = sock.send_to(KEY40, "192.168.123.255:1947");
let _ = sock.send_to(KEY40, "255.255.255.255:1947");
last_key_t = t;
}
// --- 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 {
// 本分支收集结束 → 判定
collecting = false;
if let Some((lo, hi)) = current_branch.take() {
if collision {
let mid = lo + (hi - lo) / 2;
stack.push((mid + 1, hi));
stack.push((lo, mid));
println!(" [S] 分支冲突 → 二分");
} else if found.len() == 1 && found_valid(&found[0]) {
let uid = found[0];
println!(" [S] ★发现灯具 {}", uid_s(&uid));
if !fixtures.contains(&uid) { fixtures.push(uid); }
// 参数扫描队列在发现完成后按 PID 填充 (见下)
// DISC_MUTE 该灯具
tn = tn.wrapping_add(1);
let f = build_rdm_frame(&uid, tn, qseq, 0x10, 0x0002, &[]);
qseq += 1;
let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst);
} else if found.len() > 1 {
let mid = lo + (hi - lo) / 2;
stack.push((mid + 1, hi));
stack.push((lo, mid));
}
found.clear();
collision = false;
}
}
if collecting {
// 收集窗口内每 150ms 重发同分支查询 (真主机连发节奏)
if last_rdm.elapsed() >= Duration::from_millis(150) {
last_rdm = Instant::now();
if let Some((lo, hi)) = current_branch {
let mut pd = Vec::with_capacity(12);
pd.extend_from_slice(&u64_uid(lo));
pd.extend_from_slice(&u64_uid(hi));
tn = tn.wrapping_add(1);
let mut f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0001, &pd);
qseq += 1;
f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes());
seq += 1;
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);
}
}
}
if !collecting {
if !unmuting {
// DISC_UN_MUTE 广播
tn = tn.wrapping_add(1);
let f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0003, &[]);
qseq += 1;
let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst);
unmuting = true;
println!("[S] DISC_UN_MUTE 已发");
} else if let Some((lo, hi)) = stack.pop() {
let mut pd = Vec::with_capacity(12);
pd.extend_from_slice(&u64_uid(lo));
pd.extend_from_slice(&u64_uid(hi));
tn = tn.wrapping_add(1);
let mut f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0001, &pd);
qseq += 1;
// SL 实测: RDM 请求成对发送 (flag=00 + flag=01 副本, 背靠背 26μs)
f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes());
seq += 1;
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;
collect_until = Instant::now() + Duration::from_millis(500);
} else {
if fixtures.is_empty() && t < 90.0 {
// 未发现 → 重新压栈全范围, 循环重试 (SL 实测: 认领后 ~27.5s RDM 才开始被代理!)
stack.push((0, 0xFFFF_FFFF_FFFF));
} else {
discovering = false;
println!("[S] 发现完成: {} 台灯具", fixtures.len());
}
}
}
}
// --- 收包 (非阻塞): 脉冲/参数应答 ---
while let Ok((n, src)) = sock.recv_from(&mut buf) {
if src.ip().to_string() != target { continue; }
let b = &buf[..n];
if let Some((is_pulse, d)) = rdm_region(b) {
if is_pulse && collecting {
match decode_pulse(d) {
Some(uid) => { if !found.contains(&uid) { found.push(uid); } }
None => collision = true,
}
} else if !is_pulse && d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
let ruid = [d[9], d[10], d[11], d[12], d[13], d[14]];
let addr = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]);
let footprint = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]);
// 成对副本去重 (flag=00/01 各上行一份, 背靠背到达)
if let Some((lu, lp, lt)) = last_reply {
if lu == ruid && lp == 0x0060 && lt.elapsed() < Duration::from_millis(200) { continue; }
}
last_reply = Some((ruid, 0x0060, Instant::now()));
if !answered.contains(&(ruid, 0x0060)) { answered.push((ruid, 0x0060)); }
last_sweep_rx = Instant::now();
if let Some(e) = devinfo.iter_mut().find(|x| x.uid == ruid) {
if e.addr != addr { println!(" [S] ★{} DMX地址变更: {} → {}", uid_s(&ruid), e.addr, addr); }
if e.footprint != footprint { println!(" [S] ★{} 通道数变更: {} → {}", uid_s(&ruid), e.footprint, footprint); }
e.addr = addr; e.footprint = footprint;
}
if let Some(ae) = addr_by_uid.iter_mut().find(|(u, _)| *u == ruid) { ae.1 = addr; }
else { addr_by_uid.push((ruid, addr)); }
if param_summary_done {
// 周期状态轮询: 每台一行紧凑状态
println!(" [S] 轮询#{} {} @DMX{} {}ch", status_poll_count, uid_s(&ruid), addr, footprint);
} else {
// E1.20 §10.4.1 DEVICE_INFO PD: [0]协议主 [1]协议次 [2..4]型号 [4..6]产品类
// [6..10]软件版 [10..12]footprint [12]personality [13..15]子设备数 [15..17]传感器数
let pmaj = d[0x18]; let pmin = d[0x18 + 1];
let model = u16::from_be_bytes([d[0x18 + 2], d[0x18 + 3]]);
let pers = d[0x18 + 12];
let subdev = u16::from_be_bytes([d[0x18 + 13], d[0x18 + 14]]);
let sensors = u16::from_be_bytes([d[0x18 + 15], d[0x18 + 16]]);
println!(" [S] ★{} DEVICE_INFO: 协议{}.{}/模型{:04x}/固件{:02x}{:02x}{:02x}{:02x} footprint={} personality=0x{:02x} 子设备={} 传感器={}",
uid_s(&ruid), pmaj, pmin, model, d[0x18+6], d[0x18+7], d[0x18+8], d[0x18+9],
footprint, pers, subdev, sensors);
println!(" [S] PD({:02x}): {} @addr={}", d.len() - 0x18, hexs(&d[0x18..]), addr);
let sw = [d[0x18 + 6], d[0x18 + 7], d[0x18 + 8], d[0x18 + 9]];
if let Some(e) = devinfo.iter_mut().find(|x| x.uid == ruid) {
e.proto_major = pmaj; e.proto_minor = pmin; e.model = model; e.sw = sw; e.sensors = sensors;
} else {
devinfo.push(DevInfo { uid: ruid, addr, footprint, proto_major: pmaj, proto_minor: pmin,
model, sw, sensors, label: None, model_desc: None, sw_label: None, manufacturer: None });
}
}
} else if !is_pulse && d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0xF0] {
let ruid = [d[9], d[10], d[11], d[12], d[13], d[14]];
let addr = u16::from_be_bytes([d[0x18], d[0x19]]);
if let Some((lu, lp, lt)) = last_reply {
if lu == ruid && lp == 0x00F0 && lt.elapsed() < Duration::from_millis(200) { continue; }
}
last_reply = Some((ruid, 0x00F0, Instant::now()));
if !answered.contains(&(ruid, 0x00F0)) { answered.push((ruid, 0x00F0)); }
last_sweep_rx = Instant::now();
println!(" [S] ★{} DMX_START_ADDRESS = {}", uid_s(&ruid), addr);
if let Some(e) = devinfo.iter_mut().find(|x| x.uid == ruid) { e.addr = addr; }
if let Some(ae) = addr_by_uid.iter_mut().find(|(u, _)| *u == ruid) { ae.1 = addr; }
else { addr_by_uid.push((ruid, addr)); }
} else if !is_pulse && d.len() >= 0x18 && d[0] == 0xCC
&& matches!(g16(d, 0x15), 0x0081 | 0x0082 | 0x00C0 | 0x0080) {
let ruid = [d[9], d[10], d[11], d[12], d[13], d[14]];
let pid = g16(d, 0x15);
if let Some((lu, lp, lt)) = last_reply {
if lu == ruid && lp == pid && lt.elapsed() < Duration::from_millis(200) { continue; }
}
last_reply = Some((ruid, pid, Instant::now()));
if !answered.contains(&(ruid, pid)) { answered.push((ruid, pid)); }
last_sweep_rx = Instant::now();
let (name, field) = match pid {
0x0081 => ("DEVICE_LABEL", 0usize),
0x0082 => ("MODEL_DESCRIPTION", 1usize),
0x00C0 => ("SOFTWARE_VERSION", 2usize),
_ => ("MANUFACTURER_LABEL", 3usize),
};
if d.len() > 0x18 {
let txt: String = d[0x18..].iter().map(|&c| if (0x20..0x7f).contains(&c) { c as char } else { '.' }).collect();
println!(" [S] ★{} {} = '{}'", uid_s(&ruid), name, txt);
if let Some(e) = devinfo.iter_mut().find(|x| x.uid == ruid) {
let owned = txt.trim_end_matches('.').to_string();
match field { 0 => e.label = Some(owned), 1 => e.model_desc = Some(owned),
2 => e.sw_label = Some(owned), _ => e.manufacturer = Some(owned) }
}
} else {
println!(" [S] ★{} {} = (无 PD, PDL=0x{:02x})", uid_s(&ruid), name, d[0x17]);
}
}
}
}
// --- 参数扫描 (发现完成后, 带 2 轮重试) + 周期 RDM 状态轮询 ---
const SWEEP_PIDS: [u16; 6] = [0x0060, 0x00F0, 0x0081, 0x0082, 0x00C0, 0x0080];
if !discovering && get_queue.is_empty() && !fixtures.is_empty() && !param_sweep_started {
param_sweep_started = true;
for uid in fixtures.iter() {
for pid in SWEEP_PIDS { get_queue.push((*uid, pid)); }
}
println!("[S] 参数扫描开始: {} 台 × {} PID", fixtures.len(), SWEEP_PIDS.len());
}
if !discovering && !get_queue.is_empty() && last_rdm.elapsed() >= Duration::from_millis(120) {
last_rdm = Instant::now();
let (uid, pid) = get_queue.remove(0);
tn = tn.wrapping_add(1);
let f = build_rdm_frame(&uid, tn, qseq, 0x20, pid, &[]);
qseq += 1;
let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst);
let _ = muted;
} else if param_sweep_started && get_queue.is_empty() {
// 队列排空: 要么重试未应答项, 要么出汇总, 要么进入周期状态轮询
let unanswered: Vec<([u8; 6], u16)> = fixtures.iter()
.flat_map(|u| SWEEP_PIDS.iter().map(move |p| (*u, *p)))
.filter(|key| !answered.contains(key))
.collect();
if !param_summary_done {
if !unanswered.is_empty() && sweep_round < 2 {
sweep_round += 1;
for key in unanswered.iter() { get_queue.push(*key); }
println!("[S] 参数扫描重试 第{}轮 ({} 项未应答)", sweep_round, unanswered.len());
} else if unanswered.is_empty() || last_sweep_rx.elapsed() >= Duration::from_millis(2500) {
param_summary_done = true;
next_status_poll = Instant::now() + Duration::from_secs(5);
println!("[S] ===== 参数汇总 =====");
for f in devinfo.iter() {
println!(" [S] {} @DMX{} {}ch 协议{}.{}/{:04x} 固件{:02x}{:02x}{:02x}{:02x} 型号{} 厂商{} 名称{} SW:{}",
uid_s(&f.uid), f.addr, f.footprint, f.proto_major, f.proto_minor, f.model,
f.sw[0], f.sw[1], f.sw[2], f.sw[3],
f.model_desc.as_deref().unwrap_or("-"),
f.manufacturer.as_deref().unwrap_or("-"),
f.label.as_deref().unwrap_or("-"),
f.sw_label.as_deref().unwrap_or("-"));
}
println!("[S] ==================== (每 {}s 轮询一次 RDM 状态)", STATUS_POLL_INTERVAL.as_secs());
}
} else if Instant::now() >= next_status_poll && !fixtures.is_empty() {
status_poll_count += 1;
for uid in fixtures.iter() { get_queue.push((*uid, 0x0060)); }
next_status_poll = Instant::now() + STATUS_POLL_INTERVAL;
}
}
// --- DMX 帧 @30fps ---
let mut p1 = [0u8; 512];
let mut p2 = [0u8; 512];
if !addr_by_uid.is_empty() {
for (k, &(_, a)) in addr_by_uid.iter().enumerate() {
let phase = t * 1.2 + k as f32 * 2.1;
let v = (((phase.sin() * 0.5 + 0.5) * 255.0) * (t * 0.5).min(1.0)) as u8; // 淡入
let ch = a.max(1) - 1;
if ch < 512 { p1[ch as usize] = v; } else if ch < 1024 { p2[(ch - 512) as usize] = v; }
}
}
let r1 = rng.next();
let r2 = rng.next();
let mut f = tpl.clone();
f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes());
f[0x2E] = 2; f[0x2F] = 0;
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, b12));
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&p2, r2, b12));
let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst);
seq += 1;
sent += 1;
std::thread::sleep(Duration::from_millis(33));
}
println!("[S] 会话环结束: DMX {} 帧, 发现灯具 {} 台, 控制地址 {:?}", sent, fixtures.len(),
addr_by_uid.iter().map(|(u, a)| format!("{}@{}", uid_s(u), a)).collect::<Vec<_>>());
}
/// ★UDP 31772 通用中继 (udprelay 子命令): 跨网段主机验证用
/// 客户端(任意网段) ↔ sockA(listen_addr) | 每客户端独立 sockB(src_ip:0) ↔ Butler
/// 发往 Butler 的帧源 IP = src_ip (123.113, 绕过 AECH 固件源过滤);
/// Butler 应答(单播到该客户端的 sockB) 原路转发回该客户端; Butler 广播(到 sockA) 广播给所有客户端。
///
/// 2026-09-13 重构: 旧版用单个 `client` 变量记录"最后发包者", 任何客户端在
/// 请求与应答之间只要有另一个客户端发了包(如 SL 30fps 灌流 vs 低频扫描套接字),
/// 应答就会被投递给错误的套接字 —— SL 扫描因此永远收不到 DUB 上行。
/// 现在每个客户端持有独立的上行 socket, Butler 按目的端口(=该 socket)天然
/// 区分客户端, 应答无歧义回原客户端; 上限 8 客户端, 淘汰最旧。
pub fn udp_relay(listen_addr: &str, butler_ip: &str, src_ip: &str) {
let a = open_reuse(listen_addr);
a.set_read_timeout(Some(Duration::from_millis(1))).unwrap();
let bdst = format!("{}:31772", butler_ip);
let butler_sa: std::net::SocketAddr = bdst.parse().unwrap();
struct Up {
addr: std::net::SocketAddr,
sock: UdpSocket,
}
let mut ups: Vec<Up> = Vec::new();
let mut ba = [0u8; 4096];
let mut bb = [0u8; 4096];
let (mut n_fwd, mut n_rev) = (0u64, 0u64);
println!("[RELAY] {} <-> Butler {} (源伪装 {}, 每客户端独立上行), Ctrl+C 停止", listen_addr, bdst, src_ip);
loop {
// Butler → 各客户端: 逐上行 socket 收(非阻塞, 无歧义归属)
for up in ups.iter_mut() {
loop {
match up.sock.recv_from(&mut bb) {
Ok((n, from)) => {
if from.ip().to_string() == butler_ip {
let _ = a.send_to(&bb[..n], up.addr);
n_rev += 1;
}
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
Err(_) => break,
}
}
}
// 客户端(或 Butler 侧广播) → listen socket
if let Ok((n, from)) = a.recv_from(&mut ba) {
// 2026-09-13: Butler 判定改为完整 socket (IP:31772)。旧逻辑只比
// 对 IP —— DUT 侧内部发现 walker 从 .68.200:*临时端口* 发出的查询
// 被误判为 Butler 事件转发给 SL,永远到不了总线。
if from == butler_sa {
// Butler 广播/事件 → 所有客户端
for up in ups.iter() {
let _ = a.send_to(&ba[..n], up.addr);
n_rev += 1;
}
} else {
match ups.iter_mut().find(|up| up.addr == from) {
Some(up) => {
let _ = up.sock.send_to(&ba[..n], &bdst);
n_fwd += 1;
}
None => {
let sock = open_reuse(&format!("{}:0", src_ip));
let _ = sock.set_nonblocking(true);
let _ = sock.send_to(&ba[..n], &bdst);
ups.push(Up { addr: from, sock });
if ups.len() > 8 {
ups.remove(0);
}
n_fwd += 1;
println!("[RELAY] 客户端 #{} = {}", ups.len(), from);
}
}
}
}
if (n_fwd + n_rev) % 2000 == 1 && n_fwd > 1 {
println!("[RELAY] 已转发 客户端→Butler {} 帧, Butler→客户端 {} 帧 ({} 客户端)", n_fwd, n_rev, ups.len());
}
}
}