enet-validator/src/tests.rs

906 lines
46 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.

// 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
}