906 lines
46 KiB
Rust
906 lines
46 KiB
Rust
// 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 (指纹对齐)
|
||
std::thread::sleep(Duration::from_millis(150));
|
||
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
|
||
}
|