// tests.rs — T1..T11 协议验证实现 (判据 = Butler 实际应答) use crate::enet::*; use std::collections::HashSet; use std::net::UdpSocket; use std::time::{Duration, Instant}; pub struct Item { pub id: &'static str, pub pass: bool, pub detail: String, } impl Item { fn new(id: &'static str, pass: bool, detail: String) -> Item { Item { id, pass, detail } } pub fn show(&self) { println!(" [{}] {} {}", if self.pass { "PASS" } else { "FAIL" }, self.id, self.detail); } } // ---------- T1: 发现请求 → Butler 应答 (§3.1→§3.2) ---------- pub fn t1_discover() -> Item { let s = open(); let req = load_template("discovery_req"); let _ = s.send_to(&req, BCAST_SUB); let _ = s.send_to(&req, BCAST_ALL); let replies = recv_window(&s, Duration::from_secs(4)); let mut detail = format!("收到{}包; ", replies.len()); for (_, b) in &replies { if b.len() == 217 && &b[0..4] == b"ENet" { let len_ok = g32(b, 0x14) == 217; let name_ok = find(b, b"Butler S2"); let mac_ok = find(b, &[0x00, 0x16, 0x1c, 0xf1, 0x2e, 0x39]); let pass = len_ok && name_ok && mac_ok; detail.push_str(&format!( "应答217B: len字段={} 名字={} MAC={}", len_ok, name_ok, mac_ok )); return Item::new("T1", pass, detail); } } detail.push_str("未收到 217B 发现应答"); Item::new("T1", false, detail) } // ---------- T2/T3/T4: 单向帧发送 → 观察会话反应 ---------- pub fn tx_observe(id: &'static str, tpl: &str, expect_reply: bool) -> Item { let s = open(); let f = load_template(tpl); let dst = if tpl == "heartbeat" { BCAST_SUB } else { BCAST_ALL }; let n = s.send_to(&f, dst).expect("send"); let replies = recv_window(&s, Duration::from_secs(3)); let mut detail = format!("发送{}B→{}; 收到{}包", n, dst, replies.len()); let mut pass = true; if expect_reply { pass = !replies.is_empty(); if !pass { detail.push_str("; 无应答"); } } Item::new(id, pass, detail) } // ---------- T5-T8: 会话 (DMX + 事件×2 + ACK配对 + 状态帧) ---------- pub struct SessionStats { pub dmx_sent: u64, pub evt_sent: u64, pub acks: u64, pub ack_matched: u64, pub status: u64, pub rdm: u64, pub other: u64, } pub fn session(secs: u64, mutate_channels: bool) -> (SessionStats, Vec<(u32, u64)>) { let s = open(); session_on(s, secs, mutate_channels, true) } /// 会话核心 (可注入 socket): takeover 模式跳过 preamble 且使用抢 bind 的 socket pub fn session_on(s: UdpSocket, secs: u64, mutate_channels: bool, preamble: bool) -> (SessionStats, Vec<(u32, u64)>) { if preamble { // --- 前置认领 (§6 时序): 发现请求 → 宣告帧 → 短暂等待 --- let disc = load_template("discovery_req"); let ann = load_template("announce"); let _ = s.send_to(&disc, BCAST_SUB); let _ = s.send_to(&disc, BCAST_ALL); let _ = s.send_to(&ann, BCAST_ALL); let _ = recv_window(&s, Duration::from_secs(2)); // 吸收发现应答/宣告反应 } let dmx_t = load_template("dmx_a"); let evt_t = load_template("event_b"); let mut dmx_seq = g16(&dmx_t, 0x1A).wrapping_add(1); let mut evt_seq = g32(&evt_t, 0x22).wrapping_add(1); let tick = Duration::from_millis(25); let start = Instant::now(); let mut st = SessionStats { dmx_sent: 0, evt_sent: 0, acks: 0, ack_matched: 0, status: 0, rdm: 0, other: 0 }; let mut recent: HashSet = HashSet::new(); let mut timeline: Vec<(u32, u64)> = Vec::new(); let mut tick_n: u64 = 0; while start.elapsed().as_secs() < secs { // --- DMX 帧 (形态A) --- let mut d = dmx_t.clone(); p16(&mut d, 0x1A, dmx_seq); let tms = tick_n * 25; for i in 0..8 { d[0x22 + i] = ((tms >> (i * 8)) & 0xFF) as u8; } if mutate_channels { let ph = start.elapsed().as_secs(); for i in 0..1024usize { d[0x3C + i] = match ph { 0..=9 => 0, 10..=19 => 255, _ => (((1024usize).wrapping_sub(i + (tick_n as usize) * 4)) & 0xFF) as u8, }; } } let _ = s.send_to(&d, BUTLER); st.dmx_sent += 1; dmx_seq = dmx_seq.wrapping_add(1); // --- 事件帧 ×2 (形态B) --- for _ in 0..2 { let mut e = evt_t.clone(); p32(&mut e, 0x22, evt_seq); recent.insert(evt_seq); if recent.len() > 64 { recent.remove(&(evt_seq.wrapping_sub(64))); } let _ = s.send_to(&e, BUTLER); st.evt_sent += 1; evt_seq = evt_seq.wrapping_add(1); } // --- 收集应答 --- for (_, b) in recv_window(&s, tick) { match classify(&b) { Rx::Ack(sq) => { st.acks += 1; if recent.contains(&sq) { st.ack_matched += 1; } timeline.push((sq, tick_n)); } Rx::Status(_) => st.status += 1, Rx::RdmResp(_, _) => st.rdm += 1, Rx::DiscoveryResp(_) | Rx::EnetType1(_) => {} Rx::Other(_) => st.other += 1, } } tick_n += 1; } (st, timeline) } pub fn t5_t8_session(secs: u64) -> Vec { println!(" 会话运行 {}s (DMX→事件×2, 25ms/tick)…", secs); let (st, _) = session(secs, true); let rate_dmx = st.dmx_sent as f64 / secs as f64; let pair = if st.acks > 0 { st.ack_matched as f64 / st.acks as f64 } else { 0.0 }; vec![ Item::new("T5", st.dmx_sent > secs * 30 && st.status > 0, format!("DMX发送={} (~{:.0}Hz) 状态帧={}", st.dmx_sent, rate_dmx, st.status)), Item::new("T6", pair >= 0.95, format!("ACK={} 配对={} ({:.1}%)", st.acks, st.ack_matched, pair * 100.0)), Item::new("T7", st.status > 0, format!("状态帧={}", st.status)), Item::new("T8", st.dmx_sent > secs * 30 && st.acks > secs, format!("会话{}s无中断", secs)), ] } // ---------- T9: 回落与恢复 ---------- pub fn t9_fallback() -> Item { println!(" 阶段1: 会话 10s…"); let (a, _) = session(10, false); println!(" 阶段2: 静默 12s (只收不发)…"); let s = open(); let quiet = recv_window(&s, Duration::from_secs(12)); let mut quiet_status = 0usize; for (_, b) in &quiet { if let Rx::Status(_) = classify(b) { quiet_status += 1; } } println!(" 阶段3: 恢复会话 8s…"); let (b, _) = session(8, false); let pass = a.acks > 5 && b.acks > 5; // 恢复后 ACK 恢复 = 重新接管成功 Item::new("T9", pass, format!( "会话前段ACK={}; 静默期收{}包(状态{}); 恢复后ACK={}", a.acks, quiet.len(), quiet_status, b.acks)) } // ---------- T12(实验): AECHA 认领重放 ---------- pub fn t12_claim_replay() -> Item { let s = open(); let tick = Duration::from_millis(25); let c1 = load_template("claim_h112"); let c2 = load_template("claim_h114"); let c3 = load_template("claim_h116"); let mut aereps = 0u64; let mut last_detail = String::new(); for (i, f) in [&c1, &c2, &c3].into_iter().enumerate() { let _ = s.send_to(f, BUTLER); let replies = recv_window(&s, Duration::from_millis(800)); let mut got = false; for (_, b) in &replies { if find(b, b"AEREP") { aereps += 1; got = true; } } last_detail.push_str(&format!("[{}] 应答{}包{} ", i + 1, replies.len(), if got { "+AEREP" } else { "" })); } // 若 3 轮完成 → 立即试探 DMX: 发 10 个 tick 看是否出现 ACK let mut acks = 0u64; if aereps >= 1 { let dmx_t = load_template("dmx_a"); let evt_t = load_template("event_b"); let mut dmx_seq = g16(&dmx_t, 0x1A).wrapping_add(1); let mut evt_seq = g32(&evt_t, 0x22).wrapping_add(1); for _ in 0..10 { let mut d = dmx_t.clone(); p16(&mut d, 0x1A, dmx_seq); let _ = s.send_to(&d, BUTLER); dmx_seq = dmx_seq.wrapping_add(1); for _ in 0..2 { let mut e = evt_t.clone(); p32(&mut e, 0x22, evt_seq); evt_seq += 1; let _ = s.send_to(&e, BUTLER); } for (_, b) in recv_window(&s, tick) { if let Rx::Ack(_) = classify(&b) { acks += 1; } } } } last_detail.push_str(&format!("; AEREP={} 试探ACK={}", aereps, acks)); Item::new("T12", acks > 0, last_detail) } /// takeover 变体: 使用外部传入的已 bind socket pub fn t13_takeover_on(s: UdpSocket, secs: u64) -> Vec { println!(" 发帧 {}s (共存模式)…", secs); let (st, _) = session_on(s, secs, true, false); let pair = if st.acks > 0 { st.ack_matched as f64 / st.acks as f64 } else { 0.0 }; vec![ Item::new("T13a", st.dmx_sent > secs * 20, format!("DMX发送={}", st.dmx_sent)), Item::new("T13b", st.acks > 0, format!("Rust收到ACK={} 配对={:.1}% 状态={}", st.acks, pair * 100.0, st.status)), ] } // ---------- T13(实验): 挂起 SymphoLight 后 Rust 抢占接管会话 ---------- pub fn t13_takeover(secs: u64) -> Vec { // 用抢 bind 的 socket 跑会话核心 (无 preamble: 认证已由 SymphoLight 完成) let s = open_reuse(BIND); println!(" 接管会话 {}s (SO_REUSEADDR 模式)…", secs); let (st, _) = session_on(s, secs, true, false); let pair = if st.acks > 0 { st.ack_matched as f64 / st.acks as f64 } else { 0.0 }; vec![ Item::new("T13a", st.dmx_sent > secs * 20, format!("接管后 DMX发送={}", st.dmx_sent)), Item::new("T13b", st.acks > 0, format!("ACK={} 配对={} ({:.1}%) 状态={}", st.acks, st.ack_matched, pair * 100.0, st.status)), ] } // ---------- 设备端模拟 v2 (Secondary IP, 与真 Butler 并存) ---------- // 双 socket: wildcard(REUSEADDR) 收广播发现; dev_ip:31772 收会话/发应答(源IP正确) pub fn device2(dev_ip: [u8; 4], dev_mac: [u8; 6], secs: u64, relay: Option<([u8; 4], u16)>) -> Vec { let mut items = Vec::new(); let bind_addr = format!("{}.{}.{}.{}:31772", dev_ip[0], dev_ip[1], dev_ip[2], dev_ip[3]); let sess = open_reuse(&bind_addr); // wildcard(REUSEADDR) 收广播发现; 注意: 必须先启动本程序、后启动 SymphoLight let disc = std::panic::catch_unwind(|| open_reuse("0.0.0.0:31772")).ok(); // AECH 中继 socket: 优先绑 113 (本机 123 网段时直接中继 .1); 非本网段则绑自身 IP (经桥中继) let relay_sock: Option = relay.map(|_| { std::panic::catch_unwind(|| open_reuse("192.168.123.113:0")) .unwrap_or_else(|_| open_reuse(&format!("{}.{}.{}.{}:0", dev_ip[0], dev_ip[1], dev_ip[2], dev_ip[3]))) }); sess.set_read_timeout(Some(Duration::from_millis(2))).unwrap(); println!(" 虚拟 Butler @ {} (MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}), 运行 {}s", bind_addr, dev_mac[0], dev_mac[1], dev_mac[2], dev_mac[3], dev_mac[4], dev_mac[5], secs); let resp_t = load_template("discovery_resp"); let aerep_t = load_template("aerep"); let ack_t = load_template("ack"); let status_t = load_template("status"); // 84B "Butl" 设备状态遥测帧 let reg89_t = load_template("register89"); // 89B 主机注册应答 (sub=02) let aere594_t = load_template("aere594"); // 594B AECH 认领应答 (sub=06, AERE) let ann144_t = load_template("announce144"); // 144B 宣告 (sub=01, 中继唤醒用) // RDM 应答模板: 5种DISC脉冲(101B) + 分支应答(111B) + 参数信息(119B) let pulse_ts = vec![load_template("rdm_pulse_0"), load_template("rdm_pulse_1"), load_template("rdm_pulse_2"), load_template("rdm_pulse_3"), load_template("rdm_pulse_4")]; let rdm111_t = load_template("rdm_resp111"); let rdm119_t = load_template("rdm_info119"); let setack68_t = load_template("rdm_setack68"); // RDM SET 应答模板 (CC=31, PDL=0, 68B, set123b 实证) // 真机 .1 完整 RDM 应答帧池 (178 唯一帧: 56/59/70/77/87B 全部应答词表) let rdm_pool = crate::enet::load_rdm_pool(); println!(" RDM 帧池: {} 帧", rdm_pool.len()); // PID→应答模板映射 (cmd=18=RDM GET, 查询 PID 在 0x50, 应答含 21) let mut pid_map: std::collections::HashMap<[u8; 2], Vec> = std::collections::HashMap::new(); // SET 写路径状态: 主机 SET (CC=0x30) 的值按 (UID, PID) 落存, 后续 GET 同 PID 回显新值 // (真机行为: SET 立即生效, 主机 +33ms GET 验证 — set123b 实证, §5.6) let mut set_store: std::collections::HashMap<([u8; 6], [u8; 2]), Vec> = std::collections::HashMap::new(); for line in include_str!("../samples/pid_map.txt").lines() { let line = line.trim(); if line.len() < 8 { continue; } let pid_hex = &line[..4]; let frame_hex = line[5..].trim(); let pb: Vec = (0..pid_hex.len()).step_by(2).map(|i| u8::from_str_radix(&pid_hex[i..i + 2], 16).unwrap()).collect(); let fb: Vec = (0..frame_hex.len()).step_by(2).map(|i| u8::from_str_radix(&frame_hex[i..i + 2], 16).unwrap()).collect(); if pb.len() == 2 { pid_map.insert([pb[0], pb[1]], fb); } } println!(" PID 映射: {} 个", pid_map.len()); let start = Instant::now(); let mut n_disc = 0u64; let mut n_aecha = 0u64; let mut n_dmx = 0u64; let mut n_evt = 0u64; let mut n_ack = 0u64; let mut n_other = 0u64; let mut n_status = 0u64; let mut status_seq = 0x62u32; let mut n_rdm_pulse = 0u64; let mut n_rdm_111 = 0u64; let mut n_rdm_119 = 0u64; let mut n_rdm_set = 0u64; let mut n_announce = 0u64; let mut pulse_idx = 0usize; let mut pulse_seq = 0x0f0bu32; let mut info_ctr = 0xc0u8; let mut pulse_step: u32 = 2; let mut pool_idx = 0usize; let mut cur_pulse = 0usize; let mut branch_ctr = 0u64; let mut cur_fixture = 0usize; let mut rdm_req_dumped = 0u32; let mut n_reg = 0u64; let mut n_claim = 0u64; let mut claim_nonce: u32 = 0x51f31ab9; let mut dmx_b12: Option = None; // AECH 质询 0x8C-0x8F 实例随机 → DMX 解密 rot 基值 let mut n_rdm_relay = 0u64; let mut n_rdm_fwd = 0u64; let mut pending_rdm_bridge = [0u8; 6]; // AECH 中继: (认领者地址, 主机IP LE, 主机MAC) — 质询转发真机后暂存 let mut pending_claim: Option<(std::net::SocketAddr, [u8;4], [u8;6])> = None; let mut last_pulse = Instant::now() - Duration::from_secs(10); let mut first_channels: Option<[u8; 32]> = None; let mut last_channels: [u8; 32] = [0; 32]; let mut ch_changes = 0u64; let mut last_ch_show = Instant::now() - Duration::from_secs(10); let mut peer: Option = None; // RDM 应答延迟队列 (真机灯具经 RS485 应答延迟 12-48ms, 中位 29ms — 瞬时应答是假指纹) let mut rdm_queue: Vec<(Instant, Vec, std::net::SocketAddr)> = Vec::new(); let mut delay_lcg: u32 = 0x9e3779b9; let mut next_delay = move |base_ms: u64, span_ms: u64| -> u64 { delay_lcg = delay_lcg.wrapping_mul(2654435761).wrapping_add(0x9e3779b9); base_ms + ((delay_lcg >> 16) as u64 % span_ms) }; // 42B 遥测: 真机 ~每 115ms 上报一次"已收事件计数" (0x20-0x23 严格递增, // 增量=事件B帧数, ss123 实证 +2959 计数 ≈ 2963 事件) let mut event_ctr: u32 = 0; let mut ack42_sent_val: u32 = 0; let mut last_ack42 = Instant::now() - Duration::from_secs(1); let mut evt_dst: Option = None; while start.elapsed().as_secs() < secs { // --- 真机无自发状态帧 (168s 仅 1 帧 42B, 已全部删除突发遥测) --- // --- 到期的 RDM 延迟应答 --- if !rdm_queue.is_empty() { let now = Instant::now(); let mut i = 0; while i < rdm_queue.len() { if rdm_queue[i].0 <= now { let (_, r, dst) = rdm_queue.remove(i); let _ = sess.send_to(&r, dst); } else { i += 1; } } } // --- 42B 遥测: 真机 ~每 115ms 上报一次"已收事件计数" (ss123 实证) --- if last_ack42.elapsed() > Duration::from_millis(115) && event_ctr != ack42_sent_val { if let Some(dst) = evt_dst { let mut a = ack_t.to_vec(); p32(&mut a, 0x20, event_ctr); let _ = sess.send_to(&a, dst); ack42_sent_val = event_ctr; last_ack42 = Instant::now(); } } let mut buf = [0u8; 2048]; // --- wildcard socket: 收广播发现 (仅初始发现阶段用) --- if let Some(ref d) = disc { if let Ok((n, src)) = d.recv_from(&mut buf) { let b = &buf[..n]; if b.len() == 180 && &b[0..4] == b"ENet" && g32(b, 4) == 1 { // 真机处理延迟 ~160ms (指纹对齐) // instant reply: SL scan ignores 150ms-delayed replies let mut r = resp_t.clone(); patch_dev(&mut r, dev_ip, dev_mac); patch_259_host(&mut r, src, b); let _ = sess.send_to(&r, src); n_disc += 1; println!(" [{}] 发现请求 type1 from {} len={}", chrono_t(start), src, b.len()); } else if b.len() >= 0x1A && &b[0..4] == b"ENet" && g32(b, 4) == 2 && g16(b, 0x18) == 1 { // 广播主机宣告 sub=01 → 也回 89B 注册帧 (从 sess 口发出) let mut r = reg89_t.clone(); let ip_le = [dev_ip[3], dev_ip[2], dev_ip[1], dev_ip[0]]; r[0x1A..0x1E].copy_from_slice(&ip_le); r[0x1E..0x24].copy_from_slice(&dev_mac); if b.len() > 0x24 { r[0x25..0x29].copy_from_slice(&b[0x1A..0x1E]); r[0x29..0x2F].copy_from_slice(&b[0x1E..0x24]); } let _ = sess.send_to(&r, src); n_reg += 1; } } } // --- 中继回程轮询: 真机 AERE 回到 relay_sock (113:随机端口) --- if let Some(rs) = relay_sock.as_ref() { if let Ok((n, src)) = rs.recv_from(&mut buf) { let b2 = &buf[..n]; if b2.len() >= 8 && &b2[0..4] == b"ENet" { // --- RDM 应答回程: 真机灯具的 type=04 应答 (脉冲59B/分支69B/参数77B) → 发给主机 --- if g32(b2, 4) == 4 && b2.len() >= 40 { if let Some(p) = peer { let mut r = b2.to_vec(); // cc01 帧内含 bridge-id (0x2D-0x32), 替换为查询的 bridge-id if r.len() > 0x33 && r[0x2A] == 0xCC { r[0x2D..0x33].copy_from_slice(&pending_rdm_bridge); } let _ = sess.send_to(&r, p); n_rdm_fwd += 1; if n_rdm_fwd <= 4 { println!(" [{}] RDM 应答回程 → 主机 ({}B)", chrono_t(start), r.len()); } } } if let (Some((pc, hip, hmac)), Some(_)) = (pending_claim, relay) { if b2.len() >= 552 && g32(b2, 4) == 2 && g16(b2, 0x18) == 6 { let mut r = b2.to_vec(); let ip_le = [dev_ip[3], dev_ip[2], dev_ip[1], dev_ip[0]]; r[0x1A..0x1E].copy_from_slice(&ip_le); r[0x41..0x45].copy_from_slice(&ip_le); r[0x1E..0x24].copy_from_slice(&dev_mac); r[0x49..0x4F].copy_from_slice(&dev_mac); r[0x25..0x28].copy_from_slice(&hip[0..3]); r[0x2C..0x2F].copy_from_slice(&hmac[3..6]); let _ = sess.send_to(&r, pc); pending_claim = None; n_claim += 1; println!(" [{}] ★★ 真机 AERE 中继成功 → {} #{}", chrono_t(start), pc, n_claim); } } } } } // --- 会话 socket: 单播发现 / AECHA / DMX / 事件 --- let (n, src) = match sess.recv_from(&mut buf) { Ok(x) => x, Err(_) => continue, }; let b = &buf[..n]; if b.len() < 8 || &b[0..4] != b"ENet" { continue; } peer = Some(src); // 记录对端(SymphoLight)用于遥测上行 let t = g32(b, 4); if t == 1 && b.len() == 180 { let mut r = resp_t.clone(); patch_dev(&mut r, dev_ip, dev_mac); patch_259_host(&mut r, src, b); let _ = sess.send_to(&r, src); n_disc += 1; println!(" [{}] 发现请求 type1 from {} len={}", chrono_t(start), src, b.len()); } else if t == 2 { let sub = if b.len() >= 0x1A { g16(b, 0x18) } else { 0 }; if sub == 5 && find(b, b"AECH") { if let (Some((rip, rport)), Some(rs)) = (relay, relay_sock.as_ref()) { // --- 中继模式: 先向中继目标发 sub=01 宣告唤醒注册态, 再转发质询 --- let hip = [b[0x1A], b[0x1B], b[0x1C], b[0x1D]]; let hmac = [b[0x1E], b[0x1F], b[0x20], b[0x21], b[0x22], b[0x23]]; pending_claim = Some((src, hip, hmac)); let raddr = format!("{}.{}.{}.{}:{}", rip[0], rip[1], rip[2], rip[3], rport); // 宣告帧: 0x1A-1D 主机IP(LE), 0x1E-23 主机MAC let mut ann = ann144_t.clone(); ann[0x1A..0x1E].copy_from_slice(&hip); ann[0x1E..0x24].copy_from_slice(&hmac); let _ = rs.send_to(&ann, &raddr); std::thread::sleep(Duration::from_millis(30)); let _ = rs.send_to(b, &raddr); println!(" [{}] AECH 质询中继 → 真机 {} (先宣告唤醒)", chrono_t(start), raddr); } else { // --- AECH 认领质询 (594B sub=05) → AERE 应答 (sub=06) --- // 质询帧内: 0x1A-1D 主机IP(LE), 0x1E-23 主机MAC, 0x28 "AECH" let mut r = aere594_t.clone(); // 设备 IP (LE) 0x1A 与 0x41; 设备 MAC 0x1E 与 0x49 let ip_le = [dev_ip[3], dev_ip[2], dev_ip[1], dev_ip[0]]; r[0x1A..0x1E].copy_from_slice(&ip_le); r[0x41..0x45].copy_from_slice(&ip_le); r[0x1E..0x24].copy_from_slice(&dev_mac); r[0x49..0x4F].copy_from_slice(&dev_mac); // AERE 0x25-27 = 主机IP前3字节(LE), 0x2C-2E = 主机MAC后3字节 if b.len() > 0x24 { r[0x25..0x28].copy_from_slice(&b[0x1A..0x1D]); r[0x2C..0x2F].copy_from_slice(&b[0x21..0x24]); } // 0xC0-0xC3 = AgiosElias G(nonce@0x88-0x8B LE) — 算法已从 SymphoLight // Ecue.Devices.dll 反编译还原 (无密钥! G(x)=x^TABLE[(x%32+1+(x*13)%30)%32]), // 主机按同式验证 AERE blob[152]。无需真机中继。 let nonce = if b.len() > 0x8C { g32(b, 0x88) } else { 0 }; p32(&mut r, 0xC0, crate::aech::agios_g(nonce)); // 0x8C-0x8F = AgiosElias 实例随机 B12 — DMX 帧混淆 rot=(B12^r)&511 的会话基值 if b.len() > 0x90 { dmx_b12 = Some(g32(b, 0x8C)); } let _ = sess.send_to(&r, src); n_claim += 1; println!(" [{}] ★ AECH 认领质询 → 本地计算 AERE (G({:08x})={:08x}) #{}", chrono_t(start), nonce, crate::aech::agios_g(nonce), n_claim); } } else if sub == 1 { // --- 主机身份宣告 sub=01 (102B) → 89B 注册应答 sub=02 --- // 宣告内: 0x1A-1D 主机IP(LE), 0x1E-23 主机MAC let mut r = reg89_t.clone(); let ip_le = [dev_ip[3], dev_ip[2], dev_ip[1], dev_ip[0]]; r[0x1A..0x1E].copy_from_slice(&ip_le); r[0x1E..0x24].copy_from_slice(&dev_mac); if b.len() > 0x24 { r[0x25..0x29].copy_from_slice(&b[0x1A..0x1E]); // 主机IP(LE) r[0x29..0x2F].copy_from_slice(&b[0x1E..0x24]); // 主机MAC } let _ = sess.send_to(&r, src); n_reg += 1; if n_reg <= 3 { println!(" [{}] 主机宣告 sub=01 → 回 89B 注册帧", chrono_t(start)); } } else { // --- 已认领心跳 sub=03 (36B, 每~6s): 真机从不应答 (cold2 实证), 静默 --- n_status += 1; // 仅计数 } } else if t == 4 && b.len() == 1084 { if g16(b, 0x20) != 0x0101 { event_ctr = event_ctr.wrapping_add(1); // 事件B帧计数 (含 RDM 查询, 42B 遥测用) evt_dst = Some(src); } if g16(b, 0x20) == 0x0101 { n_dmx += 1; // DMX 数据经 zPprv0TXRRW 逐帧混淆 (§4.1): r=0x22-0x25 LE, rot=(B12^r)&511 let ch16 = match dmx_b12 { Some(b12) => { let r1 = g32(b, 0x22); let plain = crate::aech::agios_dmx_decrypt(&b[0x3C..0x3C + 512], r1, b12); hexs(&plain[..16]) } None => hexs(&b[0x3C..0x3C + 16]), // 未认领前: 原始线值 }; let mut ch = [0u8; 32]; ch.copy_from_slice(&b[0x3C..0x3C + 32]); if first_channels.is_none() { first_channels = Some(ch); } if ch != last_channels { ch_changes += 1; last_channels = ch; } if last_ch_show.elapsed() > Duration::from_secs(5) { last_ch_show = Instant::now(); println!(" [{}] DMX #{} CH1-16: {}", chrono_t(start), n_dmx, ch16); } } else if b.len() > 0x44 && b[0x3C] == 0x01 && (b[0x3D] == 0x24 || b[0x3D] == 0x18 || b[0x3D] == 0x19 || b[0x3D] == 0x1a) { // --- RDM 请求 (0x20=0200, 0x3C=01, 0x3D=cmd, 0x3E+=目标UID) --- let mut uid = [0u8; 6]; uid.copy_from_slice(&b[0x3E..0x44]); // RDM 一律本地生成式应答 (全新UID灯具); relay 仅用于 AECH 认领 if rdm_req_dumped < 3 { rdm_req_dumped += 1; let dump_len = std::cmp::min(0x80, b.len()); println!(" [RDM-REQ#{}] len={} hex[0..{}]={}", rdm_req_dumped, b.len(), dump_len, hexs(&b[..dump_len])); } if b[0x3D] == 0x19 && rdm_req_dumped < 8 { rdm_req_dumped += 1; let dump_len = std::cmp::min(0x90, b.len()); println!(" [RDM-19#{}] len={} hex[0..{}]={}", rdm_req_dumped, b.len(), dump_len, hexs(&b[..dump_len])); } if b[0x30] != 0x00 { // flag=01 = DMX 内嵌副本帧: 真机完全不答, 忽略 } else if b[0x3D] == 0x24 { // --- DISC_UNIQUE_BRANCH: 分支范围 lo@0x53-0x58 hi@0x59-0x5E (PID 0001@0x50, PDL=12@0x52) --- // 真机灯具只应答 UID 落在分支范围内的查询 (ss123 实证二分收敛); // 此前无视范围全部应答 → 主机二分水远无法排空分支 → 51/s 发现洪水, 稳态轮询不启动 if b.len() > 0x5E { let lo = &b[0x53..0x59]; let hi = &b[0x59..0x5F]; for fx in FIXTURES.iter() { if &fx[..] >= lo && &fx[..] <= hi { let pulse = rdm_encode_uid(fx); let mut r: Vec = Vec::with_capacity(59); r.extend_from_slice(&pulse_ts[0][..0x2A]); // 模板头 // 脉冲 seq = 设备本地自增序号 (真机行为, 非回显!) p32(&mut r, 0x20, pulse_seq); pulse_seq = pulse_seq.wrapping_add(pulse_step); pulse_step = 1 + (pulse_step * 7 + 0x1d) % 3; // 每帧 +1..3 r.extend_from_slice(&pulse); let due = Instant::now() + Duration::from_millis(next_delay(2, 40)); rdm_queue.push((due, r, src)); n_rdm_pulse += 1; } } if n_rdm_pulse <= 5 || n_rdm_pulse % 100 == 0 { println!(" [RDM] 分支发现范围内应答 #{}", n_rdm_pulse); } } } else if b[0x3D] == 0x18 && uid == [0xff; 6] { // 广播 UID 的发现命令 (广播 MUTE 0003 等): 真机不应答, 静默 } else if b[0x3D] == 0x18 { // cmd=18 = RDM GET. PID 在 0x50-0x51. // PID=0002 = DISC_UN_MUTE → 70B ACK (CC=11, PDL=2, PD=0000) — 冷启动抓包实证 let pid = if b.len() > 0x51 { [b[0x50], b[0x51]] } else { [0, 0] }; let mirror = mirror_uid(&uid); // 优先帧池中镜像灯具的同 PID 应答 (GET_RESP=0x21, 数据自洽); 其次 pid_map; 兜底 111 let pool_hit = rdm_pool.iter().find(|f| f.len() >= 0x42 && f[0x2A] == 0xCC && f[0x33..0x39] == mirror && f[0x3E] == 0x21 && f[0x3F] == pid[0] && f[0x40] == pid[1]); let mut r = if let Some(t) = pool_hit { t.clone() } else if let Some(t) = pid_map.get(&pid) { t.clone() } else { rdm111_t.clone() }; if r.len() > 0x39 { r[0x33..0x39].copy_from_slice(&uid); } // src UID = 虚拟灯具 // dest UID (0x2D-0x32) = 查询方 bridge-id; seq 回显; TN 回显查询 TN(0x4A) if b.len() > 0x4A && r.len() > 0x33 { r[0x2D..0x33].copy_from_slice(&b[0x44..0x4A]); } r[0x20] = b[0x22]; r[0x21] = b[0x23]; if b.len() > 0x4B && r.len() > 0x3A { r[0x39] = b[0x4A]; } // SET 过的 PID 回显新值 (真机: SET 立即生效, §5.6) if let Some(v) = set_store.get(&(uid, pid)) { let pdl = if r.len() > 0x41 { r[0x41] as usize } else { 0 }; if pdl == v.len() && r.len() >= 0x42 + pdl { r[0x42..0x42 + pdl].copy_from_slice(v); } } rdm_checksum(&mut r); // patch 后必须重算 (真机 1396/1396 有效) let due = Instant::now() + Duration::from_millis(next_delay(15, 25)); n_rdm_111 += 1; if n_rdm_111 <= 6 || n_rdm_111 % 100 == 0 { println!(" [RDM] GET UID={:02x}{:02x}{:02x}{:02x}{:02x}{:02x} PID={:02x}{:02x} → {}B", uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], pid[0], pid[1], r.len()); } rdm_queue.push((due, r, src)); } else if b[0x4F] == 0x30 { // --- RDM SET (CC=0x30, msgLen=0x1a+): 真机回 CC=0x31 ACK, RT=00, PDL=0, // 68B, TN/UID 回显, checksum 有效, 延迟 18-25ms (set123b 实证, §5.6) --- let mut r = setack68_t.clone(); if r.len() > 0x39 { r[0x33..0x39].copy_from_slice(&uid); } // src UID = 虚拟灯具 if b.len() > 0x4A && r.len() > 0x33 { r[0x2D..0x33].copy_from_slice(&b[0x44..0x4A]); } // dest = 查询方 r[0x20] = b[0x22]; r[0x21] = b[0x23]; // seq 回显 if b.len() > 0x4B && r.len() > 0x3A { r[0x39] = b[0x4A]; } // TN 回显 if b.len() > 0x51 && r.len() > 0x41 { r[0x3F] = b[0x50]; r[0x40] = b[0x51]; } // PID 回显 rdm_checksum(&mut r); // patch 后必须重算 let due = Instant::now() + Duration::from_millis(next_delay(15, 25)); let pid = if b.len() > 0x51 { [b[0x50], b[0x51]] } else { [0, 0] }; // 应用写入值 (PDL@0x52, PD@0x53): 后续 GET 同 PID 回显新值 (§5.6 主机验证模式) let pdl = if b.len() > 0x52 { b[0x52] as usize } else { 0 }; if pdl > 0 && b.len() >= 0x53 + pdl { set_store.insert((uid, pid), b[0x53..0x53 + pdl].to_vec()); } rdm_queue.push((due, r, src)); n_rdm_set += 1; if n_rdm_set <= 6 || n_rdm_set % 100 == 0 { println!(" [RDM] SET UID={:02x}{:02x}{:02x}{:02x}{:02x}{:02x} PID={:02x}{:02x} PD={} → 68B ACK #{}", uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], pid[0], pid[1], hexs(set_store.get(&(uid, pid)).map(|v| &v[..]).unwrap_or(&[])), n_rdm_set); } } else { // cmd=19/1a (SET类/传感器轮询 SENSOR_VALUE 0201): PID@0x50, PD@0x53 // 按镜像灯具 + PID + PD[0](传感器序号) 从帧池取自恰应答 let pid = if b.len() > 0x51 { [b[0x50], b[0x51]] } else { [0, 0] }; let pd0 = if b.len() > 0x53 { b[0x53] } else { 0 }; let mirror = mirror_uid(&uid); let pool_hit = rdm_pool.iter().find(|f| f.len() >= 0x43 && f[0x2A] == 0xCC && f[0x33..0x39] == mirror && f[0x3F] == pid[0] && f[0x40] == pid[1] && f[0x42] == pd0); let mut r = if let Some(t) = pool_hit { t.clone() } else { rdm119_t.clone() }; if r.len() > 0x39 { r[0x33..0x39].copy_from_slice(&uid); } if b.len() > 0x4A && r.len() > 0x33 { r[0x2D..0x33].copy_from_slice(&b[0x44..0x4A]); } r[0x20] = b[0x22]; r[0x21] = b[0x23]; if b.len() > 0x4B && r.len() > 0x3A { r[0x39] = b[0x4A]; } rdm_checksum(&mut r); let due = Instant::now() + Duration::from_millis(next_delay(15, 25)); let rlen = r.len(); rdm_queue.push((due, r, src)); n_rdm_119 += 1; if n_rdm_119 <= 6 || n_rdm_119 % 100 == 0 { println!(" [RDM] 参数/传感器 UID={:02x}{:02x}{:02x}{:02x}{:02x}{:02x} PID={:02x}{:02x} PD0={:02x} → {}B #{}", uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], pid[0], pid[1], pd0, rlen, n_rdm_119); } } } else { // 非 RDM 事件B帧: 真机只计数, 由 42B 周期遥测上报, 无即时 ACK (ss123 实证) n_evt += 1; } } else { n_other += 1; if n_other <= 12 { println!(" [?] 其他帧 src={} len={} type={} hex[0..48]={}", src, b.len(), t, hexs(&b[..std::cmp::min(48, b.len())])); } } } items.push(Item::new("U1", n_disc > 0, format!("发现应答={} 遥测={} RDM脉冲={} RDM111={} RDM119={}", n_disc, n_status, n_rdm_pulse, n_rdm_111, n_rdm_119))); items.push(Item::new("U2", n_aecha > 0, format!("AECHA 认领交换={} 轮 (AEREP 已回)", n_aecha))); items.push(Item::new("U3", n_dmx > 0, format!("明文 DMX 帧接收={}", n_dmx))); items.push(Item::new("U4", n_ack > 0, format!("事件帧={} ACK发送={}", n_evt, n_ack))); items.push(Item::new("U5", n_dmx > secs * 10, format!( "通道值: 首32CH={:?} 变化={} 其他帧={}", first_channels.unwrap_or([0; 32]), ch_changes, n_other))); items } // ---------- RDM 广播发现重放探测: 向目标重放真机 RDM 广播请求, 抓取并分类应答 ---------- pub fn rdm_probe(target: &str, secs: u64) -> Item { let req_t = load_template("rdm_disc_req"); let bind = "0.0.0.0:0"; let s = UdpSocket::bind(bind).expect("rdmprobe bind 失败"); s.set_broadcast(true).unwrap(); s.set_read_timeout(Some(Duration::from_millis(120))).unwrap(); let dst = format!("{}:31772", target); println!(" rdmprobe → {} 运行 {}s", dst, secs); let start = Instant::now(); let mut seq: u32 = 0x090f; let mut sent = 0u64; let mut resp_lens: std::collections::HashMap = std::collections::HashMap::new(); let mut first_resp: Option> = None; let mut delays: Vec = Vec::new(); let mut last_send = Instant::now() - Duration::from_secs(10); let mut pending: Option<(Instant, u32)> = None; while start.elapsed().as_secs() < secs { // 每 500ms 发一次请求 if last_send.elapsed() > Duration::from_millis(500) { let mut r = req_t.clone(); p32(&mut r, 0x20, seq); let _ = s.send_to(&r, &dst); pending = Some((Instant::now(), seq)); seq = seq.wrapping_add(1); sent += 1; last_send = Instant::now(); } let mut buf = [0u8; 2048]; match s.recv_from(&mut buf) { Ok((n, src)) => { let b = &buf[..n].to_vec(); *resp_lens.entry(n).or_insert(0) += 1; if first_resp.is_none() { first_resp = Some(b.clone()); } if let Some((t0, sq)) = pending { if n >= 0x22 && g32(b, 4) == 4 { let echo = g16(b, 0x20); let expect = ((sq >> 16) as u16) ^ 0; // seq低16位在0x22 let _ = expect; delays.push((t0.elapsed().as_millis() as u32)); println!(" [{}] 应答 src={} len={} seqEcho=0x{:04x} (请求seq低16=0x{:04x}) 延迟={}ms", chrono_t(start), src, n, echo, (sq & 0xffff) as u16, t0.elapsed().as_millis()); } else { println!(" [{}] 应答 src={} len={} type={}", chrono_t(start), src, n, if n>=8 { g32(b,4) } else { 0 }); } pending = None; } } Err(_) => {} } } let mut lens_desc = String::new(); let mut keys: Vec<_> = resp_lens.keys().cloned().collect(); keys.sort(); for k in keys { lens_desc.push_str(&format!(" {}B×{}", k, resp_lens[&k])); } let avg_delay = if delays.is_empty() { 0 } else { delays.iter().sum::() / delays.len() as u32 }; Item::new("RDMP", sent > 0 && !resp_lens.is_empty(), format!("请求={} 应答[{}] 平均延迟={}ms", sent, lens_desc.trim(), avg_delay)) } // ---------- RDM DISC 脉冲编码 (E1.20) ---------- /// 虚拟灯具: 全新 UID (同厂 0107 系列, 主机未注册过的序列号) const FIXTURES: [[u8; 6]; 3] = [ [0x01, 0x07, 0x34, 0x67, 0x01, 0x30], [0x01, 0x07, 0x34, 0x67, 0x01, 0x31], [0x01, 0x07, 0x34, 0x67, 0x01, 0x32], ]; /// 镜像映射: 虚拟 UID → 真机灯具 UID (参数数据按镜像灯具从 rdm_pool 取, /// 使 3 台虚拟灯具拥有各自自洽的 DMX 地址/标签/传感器数, 与 .1 总线同构) const FIXTURE_MIRROR: [[u8; 6]; 3] = [ [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2DxxxBx DMX101-102 23传感器 [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680SxxxGx DMX111 17传感器 [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32 4传感器 ]; fn mirror_uid(uid: &[u8; 6]) -> [u8; 6] { for (i, f) in FIXTURES.iter().enumerate() { if f == uid { return FIXTURE_MIRROR[i]; } } *uid } // E1.20 checksum: 从 CC(0x2A) 起 msgLen(0x2C) 字节的 16-bit 和, BE 写于 [0x2A+msgLen] // (冷启动抓包验证: 真机 1396/1396 全部满足此规则) fn rdm_checksum(r: &mut [u8]) { if r.len() < 0x2E || r[0x2A] != 0xCC { return; } let msg_len = r[0x2C] as usize; if r.len() < 0x2A + msg_len + 2 { return; } let mut sum: u16 = 0; for i in 0x2A..0x2A + msg_len { sum = sum.wrapping_add(r[i] as u16); } r[0x2A + msg_len] = (sum >> 8) as u8; r[0x2A + msg_len + 1] = (sum & 0xFF) as u8; } fn rdm_encode_uid(uid: &[u8; 6]) -> [u8; 17] { let mut out = [0u8; 17]; out[0] = 0xAA; // 分隔符 let mut sum: u16 = 0; for (i, &b) in uid.iter().enumerate() { let b1 = 0xAA | (b & 0x55); let b2 = 0x55 | (b & 0xAA); out[1 + 2 * i] = b1; out[2 + 2 * i] = b2; sum += b1 as u16 + b2 as u16; } let hi = (sum >> 8) as u8; let lo = (sum & 0xFF) as u8; out[13] = 0xAA | (hi & 0x55); out[14] = 0x55 | (hi & 0xAA); out[15] = 0xAA | (lo & 0x55); out[16] = 0x55 | (lo & 0xAA); out } // ---------- 259 应答的 Connected To 动态化: 从发现请求提取请求方主机 IP+MAC ---------- fn patch_259_host(r: &mut [u8], src: std::net::SocketAddr, req: &[u8]) { if let std::net::IpAddr::V4(v4) = src.ip() { let o = v4.octets(); let ip_le = [o[3], o[2], o[1], o[0]]; // host IP (0x4F-0x52) = 请求方 IP (LE) if r.len() > 0x53 { r[0x4F..0x53].copy_from_slice(&ip_le); } // host MAC (0x53-0x58): 请求里 hostIP(LE) 后 +8 处 (IP4 + 00ffffff掩码 + MAC6) for i in 0..req.len().saturating_sub(14) { if req[i..i + 4] == ip_le && req[i + 4..i + 8] == [0x00, 0xff, 0xff, 0xff] { if r.len() > 0x59 { r[0x53..0x59].copy_from_slice(&req[i + 8..i + 14]); } break; } } } } // ---------- bridge: AECH 双向桥 (非123网段设备的认领桥) ---------- // 监听 listen_ip:listen_port 收远端设备的 AECH, 以源 IP=123.113 转发给真机, 再把真机 AERE 回传给远端设备 pub fn bridge(listen_ip: [u8; 4], listen_port: u16, real_ip: [u8; 4], secs: u64) { let listen_addr = format!("{}.{}.{}.{}:{}", listen_ip[0], listen_ip[1], listen_ip[2], listen_ip[3], listen_port); let listen = open_reuse(&listen_addr); let to_real = open_reuse("192.168.123.113:0"); // 源=113, 真机才应答 let real_addr = format!("{}.{}.{}.{}:31772", real_ip[0], real_ip[1], real_ip[2], real_ip[3]); println!(" AECH 桥 @ {} ↔ {} (真机源 123.113)", listen_addr, real_addr); let mut pending: Option = None; let start = Instant::now(); let mut n_fwd = 0u64; let mut n_back = 0u64; while start.elapsed().as_secs() < secs { let mut buf = [0u8; 2048]; // 远端设备 -> 真机 (AECH) if let Ok((n, src)) = listen.recv_from(&mut buf) { let b = &buf[..n]; if b.len() >= 8 && &b[0..4] == b"ENet" && g32(b, 4) == 2 && find(b, b"AECH") { pending = Some(src); let _ = to_real.send_to(b, &real_addr); n_fwd += 1; println!(" [{}] 桥: {} → 真机 (AECH #{})", chrono_t(start), src, n_fwd); } } // 真机 -> 远端设备 (AERE) if let Ok((n, src)) = to_real.recv_from(&mut buf) { let b = &buf[..n]; if b.len() >= 8 && &b[0..4] == b"ENet" && g32(b, 4) == 2 { if let Some(p) = pending { let _ = listen.send_to(b, p); n_back += 1; println!(" [{}] 桥: 真机 → {} (AERE #{})", chrono_t(start), p, n_back); } } } } } fn chrono_t(start: Instant) -> String { format!("{:.1}s", start.elapsed().as_secs_f64()) } fn patch_dev(b: &mut [u8], ip: [u8; 4], mac: [u8; 6]) { // 找 Butler IP LE (01 7b a8 c0) 替换 — 字段为小端存储: 192.168.123.200 → c8 7b a8 c0 for i in 0..b.len().saturating_sub(4) { if b[i] == 0x01 && b[i + 1] == 0x7b && b[i + 2] == 0xa8 && b[i + 3] == 0xc0 { b[i] = ip[3]; b[i + 1] = ip[2]; b[i + 2] = ip[1]; b[i + 3] = ip[0]; break; } } // 找 Butler MAC 替换 for i in 0..b.len().saturating_sub(6) { if b[i] == 0x00 && b[i + 1] == 0x16 && b[i + 2] == 0x1c && b[i + 3] == 0xf1 && b[i + 4] == 0x2e && b[i + 5] == 0x39 { b[i..i + 6].copy_from_slice(&mac); break; } } } // ---------- T10/T11: RDM 探索 ---------- pub fn t10_t11_rdm(secs: u64) -> Vec { let mut items = Vec::new(); let s = open(); let req = load_template("rdm_req"); // T10: 发送 RDM 请求事件帧模板 (DISC 帧), 解析 Butler RDM 应答 println!(" 发送 RDM 请求模板帧并监听 {}s…", secs); let deadline = Instant::now() + Duration::from_secs(secs); let mut uids: HashSet = HashSet::new(); let mut rdm_n = 0u64; let mut disc_n = 0u64; let mut req_seq = g32(&req, 0x22); while Instant::now() < deadline { let mut r = req.clone(); p32(&mut r, 0x22, req_seq); req_seq = req_seq.wrapping_add(1); let _ = s.send_to(&r, BUTLER); for (_, b) in recv_window(&s, Duration::from_millis(60)) { match classify(&b) { Rx::RdmResp(_, rlen) => { rdm_n += 1; if rlen >= 17 && b[0x2A] == 0xAA { disc_n += 1; } // 发现脉冲 if rlen >= 18 && b[0x2A] == 0xCC { let uid = hexs(&b[0x32..0x38]); uids.insert(uid); } } _ => {} } } } items.push(Item::new("T10", rdm_n > 0, format!("RDM应答={} (发现脉冲={}, 设备UID={})", rdm_n, disc_n, uids.iter().cloned().collect::>().join(" ")))); // T11: 写地址 (PID 0x0330) —— e:cue 私有封装下 PID 未知, 仅记录探索结论 items.push(Item::new("T11", false, "写地址 SET DMX_START_ADDRESS: e:net 私有 PID 未确认, 需结合 GUI 写地址抓包比对 (见文档 §5.4)".to_string())); items }