enet-validator/src/tests.rs

906 lines
46 KiB
Rust
Raw Normal View History

// 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<u32> = 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<Item> {
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<Item> {
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<Item> {
// 用抢 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<Item> {
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<UdpSocket> = 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<PID>)
let mut pid_map: std::collections::HashMap<[u8; 2], Vec<u8>> = 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<u8>> = 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<u8> = (0..pid_hex.len()).step_by(2).map(|i| u8::from_str_radix(&pid_hex[i..i + 2], 16).unwrap()).collect();
let fb: Vec<u8> = (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<u32> = 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<std::net::SocketAddr> = None;
// RDM 应答延迟队列 (真机灯具经 RS485 应答延迟 12-48ms, 中位 29ms — 瞬时应答是假指纹)
let mut rdm_queue: Vec<(Instant, Vec<u8>, 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<std::net::SocketAddr> = 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<seq16>, 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<u8> = 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<usize, u64> = std::collections::HashMap::new();
let mut first_resp: Option<Vec<u8>> = None;
let mut delays: Vec<u32> = 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::<u32>() / 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<std::net::SocketAddr> = 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<Item> {
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<String> = 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::<Vec<_>>().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
}