2026-09-11 12:03:43 +08:00
|
|
|
|
//! hostdemo.rs — 主机侧等效示例程序 (2026-09-10): 认领真机 + RDM 发现 + 加密 DMX 控制
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! 全流程不依赖 SymphoLight:
|
|
|
|
|
|
//! 1. 发现 (type=1, 180B) → 217B 应答
|
|
|
|
|
|
//! 2. sub=01 宣告 → 89B 注册应答
|
|
|
|
|
|
//! 3. AECH 认领 ×3 轮 (nonce/B12 自选, AERE[0xC0]==G(nonce) 逐轮验证)
|
|
|
|
|
|
//! ★ 真机 .1 只应答源 IP=192.168.123.113 的 AECH (固件过滤, §3.7)
|
|
|
|
|
|
//! 4. RDM 发现: E1.20 DISC_UNIQUE_BRANCH 二分遍历 + DISC 脉冲解码 (§5.4 逆运算)
|
|
|
|
|
|
//! → 每 UID 发 DISC_MUTE(0002) 收 70B ACK → GET DEVICE_INFO/DMX_START_ADDRESS/DEVICE_LABEL
|
|
|
|
|
|
//! 5. DMX 控制: 30fps zPprv0TXRRW 加密帧 (§4.1), 按发现的灯具地址做呼吸图案
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! 注意: 真机控灯! 且 SymphoLight 若同时运行会插黑场帧 (两主机流交替 → 闪烁)。
|
|
|
|
|
|
|
|
|
|
|
|
use std::net::UdpSocket;
|
|
|
|
|
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|
|
|
|
|
|
|
|
|
|
|
use crate::aech::{agios_dmx_encrypt, agios_g};
|
|
|
|
|
|
use crate::enet::{load_template, open_reuse};
|
|
|
|
|
|
|
|
|
|
|
|
const HOST_RDM_UID: [u8; 6] = [0x45, 0x3a, 0x7f, 0xff, 0xff, 0xff]; // 与真主机一致 (§5.3)
|
|
|
|
|
|
const BCAST_UID: [u8; 6] = [0xff; 6];
|
|
|
|
|
|
|
|
|
|
|
|
/// 真机冷启动实测: AECH 认领前主机先发 102B type=2 身份帧 (cold2 @38.087)
|
|
|
|
|
|
/// 主机 IP@0x1C (LE), MAC@0x20 (模板值即本机 123 网卡)
|
|
|
|
|
|
const IDENT102: [u8; 102] = [
|
|
|
|
|
|
0x45, 0x4e, 0x65, 0x74, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x7c, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
|
0x00, 0x00, 0x01, 0x00, 0x66, 0x00, 0x00, 0x00, 0x01, 0x00, 0x71, 0x7b, 0xa8, 0xc0, 0x30, 0x0e,
|
|
|
|
|
|
0xd5, 0x59, 0xbb, 0x36, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/// ★type=02 sub=07 绑定帧 (52B, sym_claim2 @351.1/353.4 — 认领前 2s 各一发):
|
|
|
|
|
|
/// 主机IP+MAC@0x1A/0x1E, 设备IP+MAC@0x25/0x29, ffff/0000@0x2F (两发唯一差异), 6400 f401@0x31
|
|
|
|
|
|
/// (0x64=100, 0x01f4=500 — 疑似刷新率参数, 原样保留)
|
|
|
|
|
|
fn build_bind52(flag16: [u8; 2]) -> Vec<u8> {
|
|
|
|
|
|
let mut f = Vec::with_capacity(52);
|
|
|
|
|
|
f.extend_from_slice(b"ENet");
|
|
|
|
|
|
f.extend_from_slice(&2u32.to_le_bytes());
|
|
|
|
|
|
f.extend_from_slice(&[0x00, 0x00, 0x1c, 0x7c]);
|
|
|
|
|
|
f.extend_from_slice(&[0u8; 4]);
|
|
|
|
|
|
f.extend_from_slice(&[0x00, 0x00, 0x01, 0x00]);
|
|
|
|
|
|
f.extend_from_slice(&52u32.to_le_bytes());
|
|
|
|
|
|
f.extend_from_slice(&[0x07, 0x00]); // sub=07
|
|
|
|
|
|
f.extend_from_slice(&[0x71, 0x7b, 0xa8, 0xc0]); // 占位: 主机IP 逆序字节 (patch)
|
|
|
|
|
|
f.extend_from_slice(&[0x30, 0x0e, 0xd5, 0x59, 0xbb, 0x36]); // 主机MAC
|
|
|
|
|
|
f.push(0x00);
|
|
|
|
|
|
f.extend_from_slice(&[0x01, 0x7b, 0xa8, 0xc0]); // 设备IP LE = 192.168.123.1
|
|
|
|
|
|
f.extend_from_slice(&[0x00, 0x16, 0x1c, 0xf1, 0x2e, 0x39]); // 设备MAC
|
|
|
|
|
|
f.extend_from_slice(&flag16);
|
|
|
|
|
|
f.extend_from_slice(&[0x64, 0x00, 0xf4, 0x01]);
|
|
|
|
|
|
f
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn g16(b: &[u8], o: usize) -> u16 { u16::from_le_bytes([b[o], b[o + 1]]) }
|
|
|
|
|
|
fn g32(b: &[u8], o: usize) -> u32 { u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]) }
|
|
|
|
|
|
fn uid_u64(u: &[u8; 6]) -> u64 { u64::from_be_bytes([0, 0, u[0], u[1], u[2], u[3], u[4], u[5]]) }
|
|
|
|
|
|
fn u64_uid(v: u64) -> [u8; 6] { let b = v.to_be_bytes(); [b[2], b[3], b[4], b[5], b[6], b[7]] }
|
|
|
|
|
|
fn hexs(b: &[u8]) -> String { b.iter().map(|x| format!("{:02x}", x)).collect() }
|
|
|
|
|
|
fn uid_s(u: &[u8; 6]) -> String { hexs(u) }
|
|
|
|
|
|
|
|
|
|
|
|
struct Rng(u32);
|
|
|
|
|
|
impl Rng {
|
|
|
|
|
|
fn new() -> Self { Rng(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_nanos() | 1) }
|
|
|
|
|
|
fn next(&mut self) -> u32 { self.0 ^= self.0 << 13; self.0 ^= self.0 >> 17; self.0 ^= self.0 << 5; self.0 & 0x7FFF_FFFF }
|
|
|
|
|
|
fn byte(&mut self) -> u8 { (self.next() >> 8) as u8 }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// E1.20 请求构建: 在 1084B 事件帧模板上 patch RDM 消息 (§4.2/§5.3)
|
|
|
|
|
|
/// frame: 0x22 qseq(LE), 0x34=msgLen+1(LE), 0x3C 起 = SubSC/msgLen/destUID/srcUID/TN/...
|
|
|
|
|
|
fn build_rdm_frame(dest: &[u8; 6], tn: u8, qseq: u16, cc: u8, pid: u16, pd: &[u8]) -> Vec<u8> {
|
|
|
|
|
|
let mut f = load_template("rdm_disc_req"); // 1084B, flag=00 独立帧形态
|
|
|
|
|
|
let msg_len = 24 + pd.len(); // CC..PD 末 (不含 checksum)
|
|
|
|
|
|
// ★RDM 请求帧头与 DMX 帧不同 (SL 实时帧对照): 计数器/随机槽位必须清零
|
|
|
|
|
|
f[0x1A..0x20].copy_from_slice(&[0u8; 6]); // 0x1A 计数器(会话内从0) + 0x1E-0x1F
|
|
|
|
|
|
f[0x24..0x2A].copy_from_slice(&[0u8; 6]); // 0x24-0x29 (DMX 帧的 r1/r2 槽位 = 0)
|
|
|
|
|
|
f[0x22] = (qseq & 0xFF) as u8;
|
|
|
|
|
|
f[0x23] = (qseq >> 8) as u8;
|
|
|
|
|
|
f[0x30] = 0; // flag=00 独立查询帧 (真机只应答此类)
|
|
|
|
|
|
f[0x34..0x38].copy_from_slice(&((msg_len + 1) as u32).to_le_bytes());
|
|
|
|
|
|
f[0x3C] = 0x01; // SubSC
|
|
|
|
|
|
f[0x3D] = msg_len as u8;
|
|
|
|
|
|
f[0x3E..0x44].copy_from_slice(dest);
|
|
|
|
|
|
f[0x44..0x4A].copy_from_slice(&HOST_RDM_UID);
|
|
|
|
|
|
f[0x4A] = tn;
|
|
|
|
|
|
f[0x4B] = 0x00; // 独立帧 port 字段=00 (模板实证; flag=01 内嵌副本才是 01)
|
|
|
|
|
|
f[0x4C] = 0x00; // msgCount
|
|
|
|
|
|
f[0x4D] = 0; f[0x4E] = 0; // subDevice
|
|
|
|
|
|
f[0x4F] = cc;
|
|
|
|
|
|
f[0x50] = (pid >> 8) as u8;
|
|
|
|
|
|
f[0x51] = (pid & 0xFF) as u8;
|
|
|
|
|
|
f[0x52] = pd.len() as u8;
|
|
|
|
|
|
f[0x53..0x53 + pd.len()].copy_from_slice(pd);
|
|
|
|
|
|
// checksum: 含隐式 0xCC 起始字节, 从 CC 起 msgLen 字节 (不含自身), BE
|
|
|
|
|
|
let mut sum: u16 = 0xCC;
|
|
|
|
|
|
for i in 0x3C..0x3C + msg_len { sum = sum.wrapping_add(f[i] as u16); }
|
|
|
|
|
|
let cs = 0x53 + pd.len();
|
|
|
|
|
|
f[cs] = (sum >> 8) as u8;
|
|
|
|
|
|
f[cs + 1] = (sum & 0xFF) as u8;
|
|
|
|
|
|
f
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: crack all DMX/RDM frame fields; show mode now fully self-constructed
- analyze subcommand: offline replay/template field variance, diff, counter analysis
- DMX header fully decoded: port mask @0x20, port lens u16LE=512 @0x34/0x36, counter +1/frame
- RDM request frame fully decoded: counter=0, 0x20=02, 0x2A=0xCC, msgLen=24+PDL,
checksum=0xCC+sum(0x3C..0x53+PDL) @0x53+PDL, pair copy rules
- Fix historical misreadings: RDM always starts @0x3C (not variable); no session-level
frame check exists - earlier failures were wrong template + checksum off-by-one
- show mode: build_dmx_frame/build_rdm_frame2/rdm_pair_copy, zero template dependency
- Decode 47B register response and 42B event frames; doc enet_protocol_current.md rewritten
2026-09-11 12:53:36 +08:00
|
|
|
|
/// ★从零构造 DMX 数据帧 (1084B, 全字段已破解 — analyze hdr 实证):
|
|
|
|
|
|
/// 帧头全部常量已知, 仅 0x1A 计数器(+1/帧) 与 0x22/0x26 r1/r2(加密随机) 变化
|
2026-09-11 13:53:21 +08:00
|
|
|
|
pub fn build_dmx_frame(p1: &[u8; 512], p2: &[u8; 512], r1: u32, r2: u32, b12: u32, ctr: u32) -> Vec<u8> {
|
feat: crack all DMX/RDM frame fields; show mode now fully self-constructed
- analyze subcommand: offline replay/template field variance, diff, counter analysis
- DMX header fully decoded: port mask @0x20, port lens u16LE=512 @0x34/0x36, counter +1/frame
- RDM request frame fully decoded: counter=0, 0x20=02, 0x2A=0xCC, msgLen=24+PDL,
checksum=0xCC+sum(0x3C..0x53+PDL) @0x53+PDL, pair copy rules
- Fix historical misreadings: RDM always starts @0x3C (not variable); no session-level
frame check exists - earlier failures were wrong template + checksum off-by-one
- show mode: build_dmx_frame/build_rdm_frame2/rdm_pair_copy, zero template dependency
- Decode 47B register response and 42B event frames; doc enet_protocol_current.md rewritten
2026-09-11 12:53:36 +08:00
|
|
|
|
let mut f = vec![0u8; 1084];
|
|
|
|
|
|
f[0..4].copy_from_slice(b"ENet");
|
|
|
|
|
|
f[4] = 4; // type=4
|
|
|
|
|
|
f[0x0A] = 0x1c; f[0x0B] = 0x7c; // 目标端口 31772 (LE)
|
|
|
|
|
|
f[0x12] = 1; // 常量 01 00
|
|
|
|
|
|
f[0x14] = 0x3c; f[0x15] = 0x04; // 帧长 1084 (u16 LE)
|
|
|
|
|
|
f[0x18] = 1; // 常量 01 00
|
|
|
|
|
|
f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); // 帧计数器 (+1/帧, 任意起始)
|
|
|
|
|
|
f[0x20] = 1; f[0x21] = 1; // 端口使能 mask: port1+port2
|
|
|
|
|
|
f[0x22..0x26].copy_from_slice(&r1.to_le_bytes());
|
|
|
|
|
|
f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes());
|
2026-09-11 13:53:21 +08:00
|
|
|
|
f[0x2E] = 2; // 输出口对序号 (SL 抓包=01, 本机实测 01/02 均可; 关键在数据区按端口分开)
|
feat: crack all DMX/RDM frame fields; show mode now fully self-constructed
- analyze subcommand: offline replay/template field variance, diff, counter analysis
- DMX header fully decoded: port mask @0x20, port lens u16LE=512 @0x34/0x36, counter +1/frame
- RDM request frame fully decoded: counter=0, 0x20=02, 0x2A=0xCC, msgLen=24+PDL,
checksum=0xCC+sum(0x3C..0x53+PDL) @0x53+PDL, pair copy rules
- Fix historical misreadings: RDM always starts @0x3C (not variable); no session-level
frame check exists - earlier failures were wrong template + checksum off-by-one
- show mode: build_dmx_frame/build_rdm_frame2/rdm_pair_copy, zero template dependency
- Decode 47B register response and 42B event frames; doc enet_protocol_current.md rewritten
2026-09-11 12:53:36 +08:00
|
|
|
|
f[0x32] = 1; // 常量
|
|
|
|
|
|
f[0x35] = 2; f[0x37] = 2; // port1/port2 数据长度 = 512 (u16 LE ×2)
|
|
|
|
|
|
f[0x38] = 1; // 常量 01 00 00 00
|
|
|
|
|
|
f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(p1, r1, b12));
|
|
|
|
|
|
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(p2, r2, b12));
|
|
|
|
|
|
f
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// ★从零构造 RDM 请求帧 (1084B, 全字段已破解 — analyze rdm 实证):
|
|
|
|
|
|
/// 计数器恒 0; 0x20=02 为 RDM 请求标志; 0x2A=0xCC 常量; checksum=0xCC+Σ(0x3C..0x53+PDL) @0x53+PDL
|
|
|
|
|
|
/// msgLen=24+PDL (含隐式 0xCC 起始码); checksum 后任意填充 (Butler 不读)
|
|
|
|
|
|
fn build_rdm_frame2(dest: &[u8; 6], tn: u8, qseq: u16, cc: u8, pid: u16, pd: &[u8]) -> Vec<u8> {
|
|
|
|
|
|
let msg_len = 24 + pd.len();
|
|
|
|
|
|
let mut f = vec![0u8; 1084];
|
|
|
|
|
|
f[0..4].copy_from_slice(b"ENet");
|
|
|
|
|
|
f[4] = 4;
|
|
|
|
|
|
f[0x0A] = 0x1c; f[0x0B] = 0x7c;
|
|
|
|
|
|
f[0x12] = 1;
|
|
|
|
|
|
f[0x14] = 0x3c; f[0x15] = 0x04;
|
|
|
|
|
|
f[0x18] = 1;
|
|
|
|
|
|
// 0x1A-0x1F: 计数器恒 0
|
|
|
|
|
|
f[0x20] = 2; // RDM 请求标志
|
|
|
|
|
|
f[0x22..0x24].copy_from_slice(&qseq.to_le_bytes());
|
|
|
|
|
|
f[0x2A] = 0xCC; // 常量
|
|
|
|
|
|
f[0x2E] = 1;
|
|
|
|
|
|
// 0x30: 00 主帧 (成对副本=01)
|
|
|
|
|
|
f[0x34..0x38].copy_from_slice(&((msg_len + 1) as u32).to_le_bytes());
|
|
|
|
|
|
f[0x38] = 1;
|
|
|
|
|
|
// RDM 消息 @0x3C: SubSC/msgLen/dest/src/TN/port/msgCount/subDev/CC/PID/PDL/PD
|
|
|
|
|
|
f[0x3C] = 1;
|
|
|
|
|
|
f[0x3D] = msg_len as u8;
|
|
|
|
|
|
f[0x3E..0x44].copy_from_slice(dest);
|
|
|
|
|
|
f[0x44..0x4A].copy_from_slice(&HOST_RDM_UID);
|
|
|
|
|
|
f[0x4A] = tn;
|
|
|
|
|
|
f[0x4F] = cc;
|
|
|
|
|
|
f[0x50] = (pid >> 8) as u8;
|
|
|
|
|
|
f[0x51] = (pid & 0xFF) as u8;
|
|
|
|
|
|
f[0x52] = pd.len() as u8;
|
|
|
|
|
|
f[0x53..0x53 + pd.len()].copy_from_slice(pd);
|
|
|
|
|
|
let mut sum: u16 = 0xCC;
|
|
|
|
|
|
for i in 0x3C..0x53 + pd.len() { sum = sum.wrapping_add(f[i] as u16); }
|
|
|
|
|
|
let cs = 0x53 + pd.len();
|
|
|
|
|
|
f[cs] = (sum >> 8) as u8;
|
|
|
|
|
|
f[cs + 1] = (sum & 0xFF) as u8;
|
|
|
|
|
|
f
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// RDM 成对副本 (SL 实测: flag@0x30=01, port@0x4B=01, srcUID 末字节-1, checksum 不重算)
|
|
|
|
|
|
fn rdm_pair_copy(f: &[u8]) -> Vec<u8> {
|
|
|
|
|
|
let mut f2 = f.to_vec();
|
|
|
|
|
|
f2[0x30] = 0x01;
|
|
|
|
|
|
f2[0x4B] = 0x01;
|
|
|
|
|
|
f2[0x49] = f2[0x49].wrapping_sub(1);
|
|
|
|
|
|
f2
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-11 12:03:43 +08:00
|
|
|
|
/// DISC 脉冲解码 (§5.4 逆运算): data = RDM 数据区 (0x2A 起), 返回 Some(UID) 或 None(冲突/非法)
|
|
|
|
|
|
fn decode_pulse(d: &[u8]) -> Option<[u8; 6]> {
|
|
|
|
|
|
if d.len() < 17 || d[0] != 0xAA { return None; }
|
|
|
|
|
|
let mut uid = [0u8; 6];
|
|
|
|
|
|
let mut sum: u16 = 0;
|
|
|
|
|
|
for i in 0..6 {
|
|
|
|
|
|
let b1 = d[1 + 2 * i];
|
|
|
|
|
|
let b2 = d[2 + 2 * i];
|
|
|
|
|
|
// 编码有效性: b1 高位必含 0xAA 模式, b2 必含 0x55 — 违例即多机冲突
|
|
|
|
|
|
if b1 & 0xAA != 0xAA || b2 & 0x55 != 0x55 { return None; }
|
|
|
|
|
|
uid[i] = (b1 & 0x55) | (b2 & 0xAA);
|
|
|
|
|
|
sum = sum.wrapping_add(b1 as u16).wrapping_add(b2 as u16);
|
|
|
|
|
|
}
|
|
|
|
|
|
let cs_hi = (d[13] & 0x55) | (d[14] & 0xAA);
|
|
|
|
|
|
let cs_lo = (d[15] & 0x55) | (d[16] & 0xAA);
|
|
|
|
|
|
let cs = ((cs_hi as u16) << 8) | cs_lo as u16;
|
|
|
|
|
|
if cs != sum { return None; } // 冲突/损坏
|
|
|
|
|
|
Some(uid)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 从 Butler→主机 应答帧提取 RDM 数据区 (§5.1): 返回 (is_pulse, data)
|
|
|
|
|
|
fn rdm_region(b: &[u8]) -> Option<(bool, &[u8])> {
|
|
|
|
|
|
if b.len() < 0x2E || &b[0..4] != b"ENet" || g32(b, 4) != 4 { return None; }
|
|
|
|
|
|
if g16(b, 0x18) != 0x0002 { return None; }
|
|
|
|
|
|
if &b[0x1A..0x1E] != b"Butl" { return None; }
|
|
|
|
|
|
let rlen = g16(b, 0x28) as usize;
|
|
|
|
|
|
if b.len() < 0x2A + rlen { return None; }
|
|
|
|
|
|
let d = &b[0x2A..0x2A + rlen];
|
|
|
|
|
|
Some((d[0] == 0xAA, d))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
|
|
|
|
|
let dst = format!("{}:31772", target);
|
|
|
|
|
|
let sock = open_reuse(&format!("{}:31772", bind_ip)); // 源 123.113:31772 (真机过滤+主机惯例)
|
|
|
|
|
|
sock.set_read_timeout(Some(Duration::from_millis(5))).unwrap();
|
|
|
|
|
|
let mut rng = Rng::new();
|
|
|
|
|
|
let mut buf = [0u8; 4096];
|
|
|
|
|
|
|
|
|
|
|
|
println!("=== 主机侧等效示例: 目标 {} (源 {}) ===", dst, bind_ip);
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 1) 发现 ----
|
|
|
|
|
|
let disc = load_template("discovery_req");
|
|
|
|
|
|
sock.send_to(&disc, &dst).unwrap();
|
|
|
|
|
|
let mut dev_ok = false;
|
|
|
|
|
|
for _ in 0..150 {
|
|
|
|
|
|
if let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
if n >= 200 && &b[0..4] == b"ENet" && g32(b, 4) == 1 {
|
|
|
|
|
|
println!("[1] 发现应答 {}B from {} (Butler 在线)", n, src);
|
|
|
|
|
|
dev_ok = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !dev_ok { println!("[1] 发现无应答, 终止"); return; }
|
|
|
|
|
|
|
|
|
|
|
|
// replay: verbatim 重放 SL 的完整会话帧流 (sym_startup t=101.3..136), 双阶段定位 RDM 门控
|
|
|
|
|
|
if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "replay" {
|
|
|
|
|
|
let path = std::env::var("REPLAY_FILE")
|
|
|
|
|
|
.unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into());
|
|
|
|
|
|
let data = std::fs::read_to_string(&path).expect("replay file");
|
|
|
|
|
|
let mut frames: Vec<(f32, String, u16, Vec<u8>)> = Vec::new();
|
|
|
|
|
|
for line in data.lines() {
|
|
|
|
|
|
let p: Vec<&str> = line.split('|').collect();
|
|
|
|
|
|
if p.len() < 4 { continue; }
|
|
|
|
|
|
let t: f32 = p[0].parse().unwrap();
|
|
|
|
|
|
let dst = p[1].to_string();
|
|
|
|
|
|
let port: u16 = p[2].parse().unwrap();
|
|
|
|
|
|
let hex = &p[3];
|
|
|
|
|
|
let mut b = Vec::with_capacity(hex.len() / 2);
|
|
|
|
|
|
for i in (0..hex.len()).step_by(2) { b.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); }
|
|
|
|
|
|
frames.push((t, dst, port, b));
|
|
|
|
|
|
}
|
|
|
|
|
|
let t0 = frames[0].0;
|
|
|
|
|
|
for phase in ["A(无认领 verbatim)", "B(先认领再 verbatim)"] {
|
|
|
|
|
|
println!("[R] === 阶段 {}: 重放 {} 帧 / {:.1}s ===", phase, frames.len(), frames[frames.len() - 1].0 - t0);
|
|
|
|
|
|
if phase.starts_with('B') {
|
|
|
|
|
|
// 快速认领
|
|
|
|
|
|
let o2: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
|
|
|
|
|
|
let ident = load_template("announce144");
|
|
|
|
|
|
sock.send_to(&ident, &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(300));
|
|
|
|
|
|
let nonce = rng.next();
|
|
|
|
|
|
let bc = rng.next();
|
|
|
|
|
|
let mut ch = load_template("claim_h112");
|
|
|
|
|
|
ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
|
|
|
|
|
|
ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes());
|
|
|
|
|
|
ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes());
|
|
|
|
|
|
sock.send_to(&ch, &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(500));
|
|
|
|
|
|
println!("[R] 认领已发 (B12=0x{:08x})", bc);
|
|
|
|
|
|
}
|
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
|
let mut recv_n = 0u64;
|
|
|
|
|
|
let mut fi = 0;
|
|
|
|
|
|
while start.elapsed().as_secs_f32() < frames[frames.len() - 1].0 - t0 + 3.0 {
|
|
|
|
|
|
let now = start.elapsed().as_secs_f32();
|
|
|
|
|
|
while fi < frames.len() && frames[fi].0 - t0 <= now {
|
|
|
|
|
|
let (_, d, port, b) = &frames[fi];
|
|
|
|
|
|
let target_addr = format!("{}:{}", d, port);
|
|
|
|
|
|
let _ = sock.send_to(b, &target_addr);
|
|
|
|
|
|
fi += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
while let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
recv_n += 1;
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
println!(" [R] ← len={} src={} head={}", n, src, hexs(&b[..std::cmp::min(20, n)]));
|
|
|
|
|
|
}
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(2));
|
|
|
|
|
|
}
|
|
|
|
|
|
println!("[R] 阶段 {} 结束: 收到 {} 帧", phase, recv_n);
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(1500));
|
|
|
|
|
|
// 排空残留
|
|
|
|
|
|
while sock.recv_from(&mut buf).is_ok() {}
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// sls: session-continue — 不认领! 直接续用 SL 会话 (B12=0x4366937a), SL 实帧做模板
|
|
|
|
|
|
if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "sls" {
|
|
|
|
|
|
let path = std::env::var("REPLAY_FILE")
|
|
|
|
|
|
.unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into());
|
|
|
|
|
|
let data = std::fs::read_to_string(&path).expect("replay file");
|
|
|
|
|
|
let sb12: u32 = 0x4366937a; // SL 22:52 会话 B12 (Butler 会话状态绑定的值)
|
|
|
|
|
|
// 从重放清单提取模板: DMX 数据帧 / RDM branch 请求
|
|
|
|
|
|
let mut base_dmx: Option<Vec<u8>> = None;
|
|
|
|
|
|
let mut base_rdm: Option<Vec<u8>> = None;
|
|
|
|
|
|
let mut max_ctr: u32 = 0;
|
|
|
|
|
|
for line in data.lines() {
|
|
|
|
|
|
let p: Vec<&str> = line.split('|').collect();
|
|
|
|
|
|
if p.len() < 4 { continue; }
|
|
|
|
|
|
let hex = p[3];
|
|
|
|
|
|
if hex.len() < 0x50 * 2 { continue; }
|
|
|
|
|
|
let b = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap();
|
|
|
|
|
|
if hex.len() == 1084 * 2 && b(0x18) == 1 && b(0x20) == 1 && b(0x21) == 1 && b(0x3c) != 1 {
|
|
|
|
|
|
let ctr = g32(&hex.as_bytes(), 0); // placeholder never used
|
|
|
|
|
|
let _ = ctr;
|
|
|
|
|
|
let mut fb = Vec::with_capacity(1084);
|
|
|
|
|
|
for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); }
|
|
|
|
|
|
let c = u32::from_le_bytes([fb[0x1A], fb[0x1B], fb[0x1C], fb[0x1D]]);
|
|
|
|
|
|
if c > max_ctr { max_ctr = c; }
|
|
|
|
|
|
if base_dmx.is_none() { base_dmx = Some(fb); }
|
|
|
|
|
|
}
|
|
|
|
|
|
if hex.len() == 1084 * 2 && b(0x3c) == 1 && b(0x30) == 0 && b(0x50) == 0 && b(0x51) == 1 && base_rdm.is_none() {
|
|
|
|
|
|
let mut fb = Vec::with_capacity(1084);
|
|
|
|
|
|
for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); }
|
|
|
|
|
|
base_rdm = Some(fb);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let dmx_t = base_dmx.expect("no DMX template in replay file");
|
|
|
|
|
|
let rdm_t = base_rdm.expect("no RDM template in replay file");
|
|
|
|
|
|
let ctr0 = max_ctr + 10;
|
|
|
|
|
|
println!("[SLS] 会话续用: B12=0x{:08x}, DMX模板 counter 基线 {}", sb12, ctr0);
|
|
|
|
|
|
println!("[SLS] DMX模板 0x2E-0x3B: {}", hexs(&dmx_t[0x2E..0x3C]));
|
|
|
|
|
|
let known: [[u8; 6]; 3] = [
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25],
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00],
|
|
|
|
|
|
[0x08, 0x99, 0x50, 0x00, 0x26, 0xc9],
|
|
|
|
|
|
];
|
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
|
let mut ctr = ctr0;
|
|
|
|
|
|
let mut last_hb = Instant::now() - Duration::from_secs(10);
|
|
|
|
|
|
let mut last_rdm = Instant::now() - Duration::from_secs(10);
|
|
|
|
|
|
let mut step = 0usize; // 0=UN_MUTE, 1..=3=窄分支, 4+=GET
|
|
|
|
|
|
let mut addr_list: Vec<u16> = Vec::new();
|
|
|
|
|
|
let mut resp_n = 0u64;
|
|
|
|
|
|
while start.elapsed() < Duration::from_secs(secs.max(60)) {
|
|
|
|
|
|
let t = start.elapsed().as_secs_f32();
|
|
|
|
|
|
if last_hb.elapsed() >= Duration::from_secs(6) {
|
|
|
|
|
|
let _ = sock.send_to(&load_template("heartbeat"), &dst);
|
|
|
|
|
|
last_hb = Instant::now();
|
|
|
|
|
|
}
|
|
|
|
|
|
// RDM 任务每 ~700ms 一步
|
|
|
|
|
|
if t > 1.0 && last_rdm.elapsed() >= Duration::from_millis(700) {
|
|
|
|
|
|
last_rdm = Instant::now();
|
|
|
|
|
|
let mut f = rdm_t.clone();
|
|
|
|
|
|
f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes());
|
|
|
|
|
|
let mut pd: Vec<u8> = Vec::new();
|
|
|
|
|
|
let mut pid: u16 = 0x0001;
|
|
|
|
|
|
let mut dest = [0xffu8; 6];
|
|
|
|
|
|
match step {
|
|
|
|
|
|
0 => { pid = 0x0003; } // UN_MUTE 广播
|
|
|
|
|
|
1..=3 => {
|
|
|
|
|
|
let u = known[step - 1];
|
|
|
|
|
|
let v = uid_u64(&u);
|
|
|
|
|
|
pd.extend_from_slice(&u64_uid(v - 1));
|
|
|
|
|
|
pd.extend_from_slice(&u64_uid(v + 1));
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
// GET DEVICE_INFO 轮询已发现地址
|
|
|
|
|
|
if (step - 4) < known.len() {
|
|
|
|
|
|
dest = known[step - 4];
|
|
|
|
|
|
pid = 0x0060;
|
|
|
|
|
|
} else { step = 3; } // 循环重试 GET
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let msg_len = 24 + pd.len();
|
|
|
|
|
|
f[0x3C] = 0x01;
|
|
|
|
|
|
f[0x3D] = msg_len as u8;
|
|
|
|
|
|
f[0x3E..0x44].copy_from_slice(&dest);
|
|
|
|
|
|
f[0x4A] = (step * 7 + 1) as u8;
|
|
|
|
|
|
f[0x4F] = if pid == 0x0001 { 0x10 } else if pid == 0x0060 { 0x20 } else { 0x10 };
|
|
|
|
|
|
f[0x50] = (pid >> 8) as u8;
|
|
|
|
|
|
f[0x51] = (pid & 0xFF) as u8;
|
|
|
|
|
|
f[0x52] = pd.len() as u8;
|
|
|
|
|
|
f[0x53..0x53 + pd.len()].copy_from_slice(&pd);
|
|
|
|
|
|
let mut sum: u16 = 0xCC;
|
|
|
|
|
|
for i in 0x3C..0x3C + msg_len { sum = sum.wrapping_add(f[i] as u16); }
|
|
|
|
|
|
let cs = 0x53 + pd.len();
|
|
|
|
|
|
f[cs] = (sum >> 8) as u8;
|
|
|
|
|
|
f[cs + 1] = (sum & 0xFF) as u8;
|
|
|
|
|
|
let mut f2 = f.clone();
|
|
|
|
|
|
f2[0x30] = 0x01;
|
|
|
|
|
|
f2[0x49] = f2[0x49].wrapping_sub(1);
|
|
|
|
|
|
let _ = sock.send_to(&f, &dst);
|
|
|
|
|
|
let _ = sock.send_to(&f2, &dst);
|
|
|
|
|
|
println!("[SLS] RDM step={} pid={:04x} 已发 (ctr={})", step, pid, ctr);
|
|
|
|
|
|
step += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 收包
|
|
|
|
|
|
while let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if src.ip().to_string() != target { continue; }
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
resp_n += 1;
|
|
|
|
|
|
if let Some((is_pulse, d)) = rdm_region(b) {
|
|
|
|
|
|
if is_pulse {
|
|
|
|
|
|
if let Some(uid) = decode_pulse(d) {
|
|
|
|
|
|
println!(" [SLS] ★脉冲 UID={}", uid_s(&uid));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
println!(" [SLS] 脉冲(冲突/损坏)");
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
|
|
|
|
|
|
let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]);
|
|
|
|
|
|
let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]);
|
|
|
|
|
|
println!(" [SLS] ★DEVICE_INFO srcUID={} DMX地址={} 通道={}", uid_s(&[d[9], d[10], d[11], d[12], d[13], d[14]]), a, fp);
|
|
|
|
|
|
if !addr_list.contains(&a) { addr_list.push(a); }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
println!(" [SLS] RDM应答 len={}", d.len());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// DMX @30fps: 发现完成前黑场, 之后呼吸
|
|
|
|
|
|
let mut p1 = [0u8; 512];
|
|
|
|
|
|
if !addr_list.is_empty() {
|
|
|
|
|
|
for (k, &a) in addr_list.iter().enumerate() {
|
|
|
|
|
|
let phase = t * 1.2 + k as f32 * 2.1;
|
|
|
|
|
|
let v = (((phase.sin() * 0.5 + 0.5) * 255.0) * ((t - 1.0) * 0.5).min(1.0).max(0.0)) as u8;
|
|
|
|
|
|
let ch = a.max(1) - 1;
|
|
|
|
|
|
if ch < 512 { p1[ch as usize] = v; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let r1 = rng.next();
|
|
|
|
|
|
let r2 = rng.next();
|
|
|
|
|
|
let mut f = dmx_t.clone();
|
|
|
|
|
|
f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes());
|
|
|
|
|
|
f[0x22..0x26].copy_from_slice(&r1.to_le_bytes());
|
|
|
|
|
|
f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes());
|
|
|
|
|
|
f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, sb12));
|
|
|
|
|
|
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], r2, sb12));
|
|
|
|
|
|
let _ = sock.send_to(&f, &dst);
|
|
|
|
|
|
ctr += 1;
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(33));
|
|
|
|
|
|
}
|
|
|
|
|
|
println!("[SLS] 结束: 应答 {} 帧, 控制地址 {:?}", resp_n, addr_list);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// hybrid / show: 认领 + 用 SL 实帧骨架, 替换为我们的 DMX 数据 + 我们构造的 RDM
|
|
|
|
|
|
// 区别: hybrid 保留原 SL 数据地址(101/123/1 可选替换); show 固定地址1-32炫彩呼吸
|
|
|
|
|
|
let mode = std::env::var("HOSTDEMO_MODE").unwrap_or_default();
|
|
|
|
|
|
if mode == "hybrid" || mode == "show" {
|
|
|
|
|
|
let is_show = mode == "show";
|
|
|
|
|
|
let label = if is_show { "SHOW" } else { "H" };
|
feat: crack all DMX/RDM frame fields; show mode now fully self-constructed
- analyze subcommand: offline replay/template field variance, diff, counter analysis
- DMX header fully decoded: port mask @0x20, port lens u16LE=512 @0x34/0x36, counter +1/frame
- RDM request frame fully decoded: counter=0, 0x20=02, 0x2A=0xCC, msgLen=24+PDL,
checksum=0xCC+sum(0x3C..0x53+PDL) @0x53+PDL, pair copy rules
- Fix historical misreadings: RDM always starts @0x3C (not variable); no session-level
frame check exists - earlier failures were wrong template + checksum off-by-one
- show mode: build_dmx_frame/build_rdm_frame2/rdm_pair_copy, zero template dependency
- Decode 47B register response and 42B event frames; doc enet_protocol_current.md rewritten
2026-09-11 12:53:36 +08:00
|
|
|
|
// show: 全字段已破解, 零模板纯自构造; hybrid: 仍用 SL 实帧骨架做对照
|
|
|
|
|
|
let mut dmxf: Vec<Vec<u8>> = Vec::new();
|
2026-09-11 12:03:43 +08:00
|
|
|
|
let mut base_rdm: Option<Vec<u8>> = None;
|
feat: crack all DMX/RDM frame fields; show mode now fully self-constructed
- analyze subcommand: offline replay/template field variance, diff, counter analysis
- DMX header fully decoded: port mask @0x20, port lens u16LE=512 @0x34/0x36, counter +1/frame
- RDM request frame fully decoded: counter=0, 0x20=02, 0x2A=0xCC, msgLen=24+PDL,
checksum=0xCC+sum(0x3C..0x53+PDL) @0x53+PDL, pair copy rules
- Fix historical misreadings: RDM always starts @0x3C (not variable); no session-level
frame check exists - earlier failures were wrong template + checksum off-by-one
- show mode: build_dmx_frame/build_rdm_frame2/rdm_pair_copy, zero template dependency
- Decode 47B register response and 42B event frames; doc enet_protocol_current.md rewritten
2026-09-11 12:53:36 +08:00
|
|
|
|
if !is_show {
|
|
|
|
|
|
let path = std::env::var("REPLAY_FILE")
|
|
|
|
|
|
.unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into());
|
|
|
|
|
|
let data = std::fs::read_to_string(&path).expect("replay file");
|
|
|
|
|
|
for line in data.lines() {
|
|
|
|
|
|
let p: Vec<&str> = line.split('|').collect();
|
|
|
|
|
|
if p.len() < 4 { continue; }
|
|
|
|
|
|
let hex = p[3];
|
|
|
|
|
|
if hex.len() != 1084 * 2 { continue; }
|
|
|
|
|
|
let b = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap();
|
|
|
|
|
|
if b(0x18) == 1 && b(0x20) == 1 && b(0x21) == 1 && b(0x3c) != 1 {
|
|
|
|
|
|
let mut fb = Vec::new();
|
|
|
|
|
|
for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); }
|
|
|
|
|
|
dmxf.push(fb);
|
|
|
|
|
|
}
|
|
|
|
|
|
if base_rdm.is_none() && b(0x3c) == 1 && b(0x30) == 0 && b(0x50) == 0 && b(0x51) == 1 {
|
|
|
|
|
|
let mut fb = Vec::new();
|
|
|
|
|
|
for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); }
|
|
|
|
|
|
base_rdm = Some(fb);
|
|
|
|
|
|
}
|
2026-09-11 12:03:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-11 15:03:09 +08:00
|
|
|
|
// 认领 (announce144 → AECH; claim 帧纯自构造: 不透明区填 0, CLAIM_ZERO 消融已证实 Butler 不校验)
|
2026-09-11 12:03:43 +08:00
|
|
|
|
let o2: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
|
|
|
|
|
|
sock.send_to(&load_template("announce144"), &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(300));
|
|
|
|
|
|
let nonce = rng.next();
|
|
|
|
|
|
let bc = rng.next();
|
2026-09-11 15:03:09 +08:00
|
|
|
|
let o2a = [o2[0], o2[1], o2[2], o2[3]];
|
|
|
|
|
|
let ch = build_claim_frame(o2a, nonce, bc);
|
2026-09-11 12:03:43 +08:00
|
|
|
|
sock.send_to(&ch, &dst).unwrap();
|
2026-09-11 13:53:21 +08:00
|
|
|
|
// ★验证 AERE 应答 (RDM 不加密, 认领失败 RDM 照样通但 DMX 解密失败 → 灯白色兜底)
|
|
|
|
|
|
let mut aere_ok = false;
|
|
|
|
|
|
let w = Instant::now();
|
|
|
|
|
|
while w.elapsed() < Duration::from_millis(700) {
|
|
|
|
|
|
if let Ok((n, _)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
if n > 0xC4 && b.windows(4).any(|x| x == b"AERE") {
|
|
|
|
|
|
let resp = g32(b, 0xC0);
|
|
|
|
|
|
let want = agios_g(nonce);
|
|
|
|
|
|
aere_ok = resp == want;
|
|
|
|
|
|
println!("[{}] AERE resp=0x{:08x} want=0x{:08x} {}", label, resp, want, if aere_ok { "★PASS" } else { "FAIL" });
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !aere_ok { println!("[{}] ⚠ 未收到有效 AERE — 认领可能失败, DMX 将无法解密!", label); }
|
feat: crack all DMX/RDM frame fields; show mode now fully self-constructed
- analyze subcommand: offline replay/template field variance, diff, counter analysis
- DMX header fully decoded: port mask @0x20, port lens u16LE=512 @0x34/0x36, counter +1/frame
- RDM request frame fully decoded: counter=0, 0x20=02, 0x2A=0xCC, msgLen=24+PDL,
checksum=0xCC+sum(0x3C..0x53+PDL) @0x53+PDL, pair copy rules
- Fix historical misreadings: RDM always starts @0x3C (not variable); no session-level
frame check exists - earlier failures were wrong template + checksum off-by-one
- show mode: build_dmx_frame/build_rdm_frame2/rdm_pair_copy, zero template dependency
- Decode 47B register response and 42B event frames; doc enet_protocol_current.md rewritten
2026-09-11 12:53:36 +08:00
|
|
|
|
println!("[{}] 认领完成 B12=0x{:08x}; {}", label, bc,
|
|
|
|
|
|
if is_show { "地址1-32 炫彩呼吸 (零模板纯自构造 DMX+RDM)" } else { "verbatim DMX + 模板 RDM" });
|
|
|
|
|
|
let rdm_t: Option<Vec<u8>> = base_rdm;
|
2026-09-11 12:03:43 +08:00
|
|
|
|
let known: [[u8; 6]; 3] = [
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25],
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00],
|
|
|
|
|
|
[0x08, 0x99, 0x50, 0x00, 0x26, 0xc9],
|
|
|
|
|
|
];
|
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
|
let mut di = 0usize;
|
feat: crack all DMX/RDM frame fields; show mode now fully self-constructed
- analyze subcommand: offline replay/template field variance, diff, counter analysis
- DMX header fully decoded: port mask @0x20, port lens u16LE=512 @0x34/0x36, counter +1/frame
- RDM request frame fully decoded: counter=0, 0x20=02, 0x2A=0xCC, msgLen=24+PDL,
checksum=0xCC+sum(0x3C..0x53+PDL) @0x53+PDL, pair copy rules
- Fix historical misreadings: RDM always starts @0x3C (not variable); no session-level
frame check exists - earlier failures were wrong template + checksum off-by-one
- show mode: build_dmx_frame/build_rdm_frame2/rdm_pair_copy, zero template dependency
- Decode 47B register response and 42B event frames; doc enet_protocol_current.md rewritten
2026-09-11 12:53:36 +08:00
|
|
|
|
let mut qseq: u16 = 0;
|
2026-09-11 12:03:43 +08:00
|
|
|
|
let mut last_rdm = Instant::now() - Duration::from_secs(10);
|
|
|
|
|
|
let mut step = 0usize;
|
|
|
|
|
|
let mut addr_list: Vec<u16> = Vec::new();
|
|
|
|
|
|
let mut resp_n = 0u64;
|
|
|
|
|
|
let mut state: Vec<([u8; 6], u16, u16)> = Vec::new();
|
|
|
|
|
|
let mut last_status = Instant::now() - Duration::from_secs(100);
|
|
|
|
|
|
// ★消融实证: 周期宣告/发现广播是 RDM 代理开关 (nobcast 失败, nodmx 成功)
|
|
|
|
|
|
let disc = load_template("discovery_req");
|
|
|
|
|
|
let mut ann = load_template("announce");
|
|
|
|
|
|
ann[0x41..0x45].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
|
|
|
|
|
|
let mut last_bcast = Instant::now() - Duration::from_secs(10);
|
|
|
|
|
|
while secs == 0 || start.elapsed() < Duration::from_secs(secs) {
|
|
|
|
|
|
let t = start.elapsed().as_secs_f32();
|
|
|
|
|
|
if last_bcast.elapsed() >= Duration::from_millis(2500) {
|
|
|
|
|
|
let _ = sock.send_to(&disc, "255.255.255.255:31772");
|
|
|
|
|
|
let _ = sock.send_to(&ann, &format!("{}:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123").to_string() + ".255"));
|
|
|
|
|
|
last_bcast = Instant::now();
|
|
|
|
|
|
}
|
|
|
|
|
|
// verbatim DMX 循环
|
|
|
|
|
|
// DMX 数据替换
|
|
|
|
|
|
let mut p1 = [0u8; 512];
|
2026-09-11 13:53:21 +08:00
|
|
|
|
if std::env::var("SHOW_DIAG").unwrap_or_default() == "1" {
|
|
|
|
|
|
// 诊断: ch1-32 全灭3s / 全亮3s 交替 — 验证 DMX 是否到达灯具
|
|
|
|
|
|
let v = if (t as u32 / 3) % 2 == 0 { 0u8 } else { 255u8 };
|
|
|
|
|
|
for i in 0..32 { p1[i] = v; }
|
|
|
|
|
|
} else if is_show {
|
2026-09-11 12:03:43 +08:00
|
|
|
|
// show: 地址 1-32 炫彩呼吸波 (周期放慢, 更舒缓)
|
|
|
|
|
|
let breathe = (t * 0.45).sin() * 0.5 + 0.5;
|
|
|
|
|
|
for i in 0..32usize {
|
|
|
|
|
|
let wave = ((t * 0.6 + i as f32 * 0.55).sin() * 0.5 + 0.5);
|
|
|
|
|
|
p1[i] = (wave * breathe * 255.0) as u8;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if std::env::var("SHOW_PATTERN").unwrap_or_default() == "1" {
|
|
|
|
|
|
for (k, &a) in [101u16, 123, 1].iter().enumerate() {
|
|
|
|
|
|
let phase = t * 1.0 + k as f32 * 2.1;
|
|
|
|
|
|
let v = (((phase.sin() * 0.5 + 0.5) * 220.0) * (t * 0.3).min(1.0)) as u8;
|
|
|
|
|
|
p1[(a - 1) as usize] = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let rr1 = rng.next();
|
|
|
|
|
|
let rr2 = rng.next();
|
2026-09-11 13:53:21 +08:00
|
|
|
|
// show 模式: 图案同时输出到 port1+port2 (灯具可能在任一口, 实测 port2 出灯)
|
|
|
|
|
|
let mut p2 = [0u8; 512];
|
|
|
|
|
|
if is_show || std::env::var("SHOW_PORT2").unwrap_or_default() == "1" { p2 = p1; }
|
feat: crack all DMX/RDM frame fields; show mode now fully self-constructed
- analyze subcommand: offline replay/template field variance, diff, counter analysis
- DMX header fully decoded: port mask @0x20, port lens u16LE=512 @0x34/0x36, counter +1/frame
- RDM request frame fully decoded: counter=0, 0x20=02, 0x2A=0xCC, msgLen=24+PDL,
checksum=0xCC+sum(0x3C..0x53+PDL) @0x53+PDL, pair copy rules
- Fix historical misreadings: RDM always starts @0x3C (not variable); no session-level
frame check exists - earlier failures were wrong template + checksum off-by-one
- show mode: build_dmx_frame/build_rdm_frame2/rdm_pair_copy, zero template dependency
- Decode 47B register response and 42B event frames; doc enet_protocol_current.md rewritten
2026-09-11 12:53:36 +08:00
|
|
|
|
let f = if is_show {
|
|
|
|
|
|
// ★纯自构造 DMX 帧 (零模板)
|
2026-09-11 13:53:21 +08:00
|
|
|
|
build_dmx_frame(&p1, &p2, rr1, rr2, bc, di as u32)
|
2026-09-11 12:03:43 +08:00
|
|
|
|
} else {
|
feat: crack all DMX/RDM frame fields; show mode now fully self-constructed
- analyze subcommand: offline replay/template field variance, diff, counter analysis
- DMX header fully decoded: port mask @0x20, port lens u16LE=512 @0x34/0x36, counter +1/frame
- RDM request frame fully decoded: counter=0, 0x20=02, 0x2A=0xCC, msgLen=24+PDL,
checksum=0xCC+sum(0x3C..0x53+PDL) @0x53+PDL, pair copy rules
- Fix historical misreadings: RDM always starts @0x3C (not variable); no session-level
frame check exists - earlier failures were wrong template + checksum off-by-one
- show mode: build_dmx_frame/build_rdm_frame2/rdm_pair_copy, zero template dependency
- Decode 47B register response and 42B event frames; doc enet_protocol_current.md rewritten
2026-09-11 12:53:36 +08:00
|
|
|
|
let mut fx = dmxf[di % dmxf.len()].clone();
|
|
|
|
|
|
fx[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());
|
|
|
|
|
|
fx[0x22..0x26].copy_from_slice(&rr1.to_le_bytes());
|
|
|
|
|
|
fx[0x26..0x2A].copy_from_slice(&rr2.to_le_bytes());
|
|
|
|
|
|
fx[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, rr1, bc));
|
|
|
|
|
|
fx[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], rr2, bc));
|
|
|
|
|
|
fx
|
2026-09-11 12:03:43 +08:00
|
|
|
|
};
|
|
|
|
|
|
di += 1;
|
|
|
|
|
|
let _ = sock.send_to(&f, &dst);
|
|
|
|
|
|
if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); }
|
|
|
|
|
|
// RDM 任务
|
|
|
|
|
|
if t > 2.0 && last_rdm.elapsed() >= Duration::from_millis(700) {
|
|
|
|
|
|
last_rdm = Instant::now();
|
|
|
|
|
|
let mut pd: Vec<u8> = Vec::new();
|
|
|
|
|
|
let mut pid: u16 = 0x0001;
|
|
|
|
|
|
let mut dest = [0xffu8; 6];
|
|
|
|
|
|
let mut cc = 0x10u8;
|
|
|
|
|
|
match step {
|
|
|
|
|
|
0 => { pid = 0x0003; }
|
|
|
|
|
|
1..=3 => {
|
|
|
|
|
|
let v = uid_u64(&known[step - 1]);
|
|
|
|
|
|
pd.extend_from_slice(&u64_uid(v - 1));
|
|
|
|
|
|
pd.extend_from_slice(&u64_uid(v + 1));
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
if (step - 4) < known.len() { dest = known[step - 4]; pid = 0x0060; cc = 0x20; }
|
|
|
|
|
|
else { step = 3; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat: crack all DMX/RDM frame fields; show mode now fully self-constructed
- analyze subcommand: offline replay/template field variance, diff, counter analysis
- DMX header fully decoded: port mask @0x20, port lens u16LE=512 @0x34/0x36, counter +1/frame
- RDM request frame fully decoded: counter=0, 0x20=02, 0x2A=0xCC, msgLen=24+PDL,
checksum=0xCC+sum(0x3C..0x53+PDL) @0x53+PDL, pair copy rules
- Fix historical misreadings: RDM always starts @0x3C (not variable); no session-level
frame check exists - earlier failures were wrong template + checksum off-by-one
- show mode: build_dmx_frame/build_rdm_frame2/rdm_pair_copy, zero template dependency
- Decode 47B register response and 42B event frames; doc enet_protocol_current.md rewritten
2026-09-11 12:53:36 +08:00
|
|
|
|
let tn = (step * 7 + 1) as u8;
|
|
|
|
|
|
let (f, f2) = if is_show {
|
|
|
|
|
|
// ★纯自构造 RDM 帧 (零模板) + 成对副本
|
|
|
|
|
|
qseq = qseq.wrapping_add(1);
|
|
|
|
|
|
let f = build_rdm_frame2(&dest, tn, qseq, cc, pid, &pd);
|
|
|
|
|
|
let f2 = rdm_pair_copy(&f);
|
|
|
|
|
|
(f, f2)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let mut f = rdm_t.clone().unwrap();
|
|
|
|
|
|
let msg_len = 24 + pd.len();
|
|
|
|
|
|
f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());
|
|
|
|
|
|
f[0x3D] = msg_len as u8;
|
|
|
|
|
|
f[0x3E..0x44].copy_from_slice(&dest);
|
|
|
|
|
|
f[0x4A] = tn;
|
|
|
|
|
|
f[0x4F] = cc;
|
|
|
|
|
|
f[0x50] = (pid >> 8) as u8;
|
|
|
|
|
|
f[0x51] = (pid & 0xFF) as u8;
|
|
|
|
|
|
f[0x52] = pd.len() as u8;
|
|
|
|
|
|
f[0x53..0x53 + pd.len()].copy_from_slice(&pd);
|
|
|
|
|
|
let mut sum: u16 = 0xCC;
|
|
|
|
|
|
for i in 0x3C..0x3C + msg_len { sum = sum.wrapping_add(f[i] as u16); }
|
|
|
|
|
|
let cs = 0x53 + pd.len();
|
|
|
|
|
|
f[cs] = (sum >> 8) as u8;
|
|
|
|
|
|
f[cs + 1] = (sum & 0xFF) as u8;
|
|
|
|
|
|
let mut f2 = f.clone();
|
|
|
|
|
|
f2[0x30] = 0x01;
|
|
|
|
|
|
f2[0x49] = f2[0x49].wrapping_sub(1);
|
|
|
|
|
|
(f, f2)
|
|
|
|
|
|
};
|
2026-09-11 12:03:43 +08:00
|
|
|
|
let _ = sock.send_to(&f, &dst);
|
|
|
|
|
|
let _ = sock.send_to(&f2, &dst);
|
|
|
|
|
|
println!("[{}] RDM step={} pid={:04x}", label, step, pid);
|
|
|
|
|
|
step += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
while let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if src.ip().to_string() != target { continue; }
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
resp_n += 1;
|
|
|
|
|
|
if let Some((is_pulse, d)) = rdm_region(b) {
|
|
|
|
|
|
if is_show {
|
|
|
|
|
|
// show 模式维护状态表
|
|
|
|
|
|
if is_pulse {
|
|
|
|
|
|
if let Some(uid) = decode_pulse(d) {
|
|
|
|
|
|
println!(" [{}] ★脉冲 UID={}", label, uid_s(&uid));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
|
|
|
|
|
|
let uid = [d[9], d[10], d[11], d[12], d[13], d[14]];
|
|
|
|
|
|
let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]);
|
|
|
|
|
|
let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]);
|
|
|
|
|
|
if let Some(e) = state.iter_mut().find(|(u, _, _)| *u == uid) {
|
|
|
|
|
|
if e.1 != a || e.2 != fp {
|
|
|
|
|
|
println!(" [{}] ★{} 状态变更: 地址 {}→{} 通道 {}→{}", label, uid_s(&uid), e.1, a, e.2, fp);
|
|
|
|
|
|
e.1 = a; e.2 = fp;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
state.push((uid, a, fp));
|
|
|
|
|
|
println!(" [{}] ★设备注册: {} 地址={} 通道={} 传感器={}", label, uid_s(&uid), a, fp, d[0x18 + 18]);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0xF0] {
|
|
|
|
|
|
let uid = [d[9], d[10], d[11], d[12], d[13], d[14]];
|
|
|
|
|
|
let a = u16::from_be_bytes([d[0x18], d[0x19]]);
|
|
|
|
|
|
if let Some(e) = state.iter_mut().find(|(u, _, _)| *u == uid) {
|
|
|
|
|
|
if e.1 != a {
|
|
|
|
|
|
println!(" [{}] ★{} START_ADDRESS 变更: {}→{}", label, uid_s(&uid), e.1, a);
|
|
|
|
|
|
e.1 = a;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if is_pulse {
|
|
|
|
|
|
if let Some(uid) = decode_pulse(d) { println!(" [H] ★脉冲 UID={}", uid_s(&uid)); }
|
|
|
|
|
|
else { println!(" [H] 脉冲(冲突)"); }
|
|
|
|
|
|
} else if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
|
|
|
|
|
|
let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]);
|
|
|
|
|
|
let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]);
|
|
|
|
|
|
println!(" [H] ★DEVICE_INFO srcUID={} 地址={} 通道={}", uid_s(&[d[9], d[10], d[11], d[12], d[13], d[14]]), a, fp);
|
|
|
|
|
|
if !addr_list.contains(&a) { addr_list.push(a); }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
println!(" [H] RDM应答 len={}", d.len());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// show 模式每 10s 打印状态汇总
|
|
|
|
|
|
if is_show && last_status.elapsed() >= Duration::from_secs(10) {
|
|
|
|
|
|
last_status = Instant::now();
|
|
|
|
|
|
let lines: Vec<String> = state.iter().map(|(u, a, fp)| format!("{} @DMX{} ({}ch)", uid_s(u), a, fp)).collect();
|
|
|
|
|
|
println!("[SHOW] 状态: {} 台 | {} | 收帧{}", state.len(), lines.join(" | "), resp_n);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(33));
|
|
|
|
|
|
}
|
|
|
|
|
|
println!("[{}] 结束: 应答 {} 帧, 地址 {:?}", label, resp_n, if is_show { state.iter().map(|(_, a, _)| *a).collect::<Vec<_>>() } else { addr_list.clone() });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// slfull: 认领 + verbatim 重放 SL 会话(循环) + 被动解码 RDM 应答 → 发现灯具并打印地址
|
|
|
|
|
|
// (绕过未破解的变长 RDM 帧结构: 用 SL 原帧当查询, 我们解码应答)
|
|
|
|
|
|
if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "slfull" {
|
|
|
|
|
|
let path = std::env::var("REPLAY_FILE")
|
|
|
|
|
|
.unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into());
|
|
|
|
|
|
let data = std::fs::read_to_string(&path).expect("replay file");
|
|
|
|
|
|
let mut frames: Vec<(f32, String, u16, Vec<u8>)> = Vec::new();
|
|
|
|
|
|
for line in data.lines() {
|
|
|
|
|
|
let p: Vec<&str> = line.split('|').collect();
|
|
|
|
|
|
if p.len() < 4 { continue; }
|
|
|
|
|
|
let t: f32 = p[0].parse().unwrap();
|
|
|
|
|
|
let dst = p[1].to_string();
|
|
|
|
|
|
let port: u16 = p[2].parse().unwrap();
|
|
|
|
|
|
let hex = p[3];
|
|
|
|
|
|
let mut b = Vec::new();
|
|
|
|
|
|
for i in (0..hex.len()).step_by(2) { b.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); }
|
|
|
|
|
|
frames.push((t, dst, port, b));
|
|
|
|
|
|
}
|
|
|
|
|
|
let t0 = frames[0].0;
|
|
|
|
|
|
let dur = frames[frames.len() - 1].0 - t0;
|
|
|
|
|
|
// 认领
|
|
|
|
|
|
let o2: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
|
|
|
|
|
|
sock.send_to(&load_template("announce144"), &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(300));
|
|
|
|
|
|
let nonce = rng.next();
|
|
|
|
|
|
let bc = rng.next();
|
|
|
|
|
|
let mut ch = load_template("claim_h112");
|
|
|
|
|
|
ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
|
|
|
|
|
|
ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes());
|
|
|
|
|
|
ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes());
|
|
|
|
|
|
sock.send_to(&ch, &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(500));
|
|
|
|
|
|
println!("[F] 认领完成; 开始循环重放 {} 帧 ({:.1}s/轮), 被动解码 RDM 应答", frames.len(), dur);
|
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
|
let mut seen_uids: Vec<[u8; 6]> = Vec::new();
|
|
|
|
|
|
let mut addr_map: Vec<([u8; 6], u16)> = Vec::new();
|
|
|
|
|
|
let mut resp_n = 0u64;
|
|
|
|
|
|
let mut pat_ctr: u32 = 0;
|
|
|
|
|
|
while start.elapsed() < Duration::from_secs(secs.max(120)) {
|
|
|
|
|
|
let play_start = Instant::now();
|
|
|
|
|
|
for (t, d, port, b) in frames.iter() {
|
|
|
|
|
|
let target_t = Duration::from_secs_f32(t - t0);
|
|
|
|
|
|
let now = play_start.elapsed();
|
|
|
|
|
|
if target_t > now { std::thread::sleep(target_t - now); }
|
|
|
|
|
|
let addr = format!("{}:{}", d, port);
|
|
|
|
|
|
// ★DMX 数据帧: 替换为我们自己的呼吸图案 (用认领 B12 加密 → Butler 解密上灯)
|
|
|
|
|
|
let mut fb = b.clone();
|
|
|
|
|
|
if fb.len() == 1084 && fb[0x18] == 1 && fb[0x20] == 1 && fb[0x21] == 1 && fb[0x3C] != 1 {
|
|
|
|
|
|
pat_ctr += 1;
|
|
|
|
|
|
let tt = pat_ctr as f32 / 30.0;
|
|
|
|
|
|
let mut p1 = [0u8; 512];
|
|
|
|
|
|
for (k, &a) in [101u16, 123, 1].iter().enumerate() {
|
|
|
|
|
|
let phase = tt * 1.0 + k as f32 * 2.1;
|
|
|
|
|
|
let v = (((phase.sin() * 0.5 + 0.5) * 220.0) * (tt * 0.3).min(1.0)) as u8;
|
|
|
|
|
|
p1[(a - 1) as usize] = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
let r1 = rng.next();
|
|
|
|
|
|
let r2 = rng.next();
|
|
|
|
|
|
fb[0x22..0x26].copy_from_slice(&r1.to_le_bytes());
|
|
|
|
|
|
fb[0x26..0x2A].copy_from_slice(&r2.to_le_bytes());
|
|
|
|
|
|
fb[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, bc));
|
|
|
|
|
|
fb[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], r2, bc));
|
|
|
|
|
|
}
|
|
|
|
|
|
let _ = sock.send_to(&fb, &addr);
|
|
|
|
|
|
// 收包
|
|
|
|
|
|
while let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if src.ip().to_string() != target { continue; }
|
|
|
|
|
|
let bb = &buf[..n];
|
|
|
|
|
|
resp_n += 1;
|
|
|
|
|
|
if let Some((is_pulse, dd)) = rdm_region(bb) {
|
|
|
|
|
|
if is_pulse {
|
|
|
|
|
|
if let Some(uid) = decode_pulse(dd) {
|
|
|
|
|
|
if !seen_uids.contains(&uid) {
|
|
|
|
|
|
seen_uids.push(uid);
|
|
|
|
|
|
println!(" [F] ★发现灯具 UID={}", uid_s(&uid));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if dd.len() >= 0x18 + 19 && dd[0] == 0xCC && dd[0x15..0x17] == [0x00, 0x60] {
|
|
|
|
|
|
let src_uid = [dd[9], dd[10], dd[11], dd[12], dd[13], dd[14]];
|
|
|
|
|
|
let a = u16::from_be_bytes([dd[0x18 + 14], dd[0x18 + 15]]);
|
|
|
|
|
|
let fp = u16::from_be_bytes([dd[0x18 + 10], dd[0x18 + 11]]);
|
|
|
|
|
|
if !addr_map.iter().any(|(u, _)| *u == src_uid) {
|
|
|
|
|
|
addr_map.push((src_uid, a));
|
|
|
|
|
|
println!(" [F] ★{} → DMX地址 {} ({}通道)", uid_s(&src_uid), a, fp);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if dd.len() >= 0x18 + 2 && dd[0] == 0xCC && dd[0x15..0x17] == [0x00, 0xF0] {
|
|
|
|
|
|
let src_uid = [dd[9], dd[10], dd[11], dd[12], dd[13], dd[14]];
|
|
|
|
|
|
let a = u16::from_be_bytes([dd[0x18], dd[0x19]]);
|
|
|
|
|
|
if !addr_map.iter().any(|(u, _)| *u == src_uid) {
|
|
|
|
|
|
addr_map.push((src_uid, a));
|
|
|
|
|
|
println!(" [F] ★{} → DMX_START_ADDRESS={}", uid_s(&src_uid), a);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
println!("[F] 结束: 应答 {} 帧; 灯具 {} 台: {:?}; 地址: {:?}", resp_n, seen_uids.len(),
|
|
|
|
|
|
seen_uids.iter().map(uid_s).collect::<Vec<_>>(), addr_map);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// slbisect: SL 原帧逐字段突变 — 定位 Butler 拒收我们 RDM 帧的确切字段
|
|
|
|
|
|
// 突变序列: verbatim(对照) → counter → TN → qseq → 全部 → 纯我们构造
|
|
|
|
|
|
if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "slbisect" {
|
|
|
|
|
|
// 认领
|
|
|
|
|
|
let o2: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
|
|
|
|
|
|
sock.send_to(&load_template("announce144"), &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(300));
|
|
|
|
|
|
let nonce = rng.next();
|
|
|
|
|
|
let bc = rng.next();
|
|
|
|
|
|
let mut ch = load_template("claim_h112");
|
|
|
|
|
|
ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
|
|
|
|
|
|
ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes());
|
|
|
|
|
|
ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes());
|
|
|
|
|
|
sock.send_to(&ch, &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(500));
|
|
|
|
|
|
println!("[B] 认领完成 B12=0x{:08x}", bc);
|
|
|
|
|
|
// verbatim DMX 背景流 + 突变 branch 查询
|
|
|
|
|
|
let path = std::env::var("REPLAY_FILE")
|
|
|
|
|
|
.unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into());
|
|
|
|
|
|
let data = std::fs::read_to_string(&path).expect("replay file");
|
|
|
|
|
|
let mut dmxf: Vec<Vec<u8>> = Vec::new();
|
|
|
|
|
|
for line in data.lines() {
|
|
|
|
|
|
let p: Vec<&str> = line.split('|').collect();
|
|
|
|
|
|
if p.len() < 4 { continue; }
|
|
|
|
|
|
let hex = p[3];
|
|
|
|
|
|
if hex.len() != 1084 * 2 { continue; }
|
|
|
|
|
|
let byte = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap();
|
|
|
|
|
|
if byte(0x18) == 1 && byte(0x20) == 1 && byte(0x21) == 1 && byte(0x3c) != 1 {
|
|
|
|
|
|
let mut fb = Vec::new();
|
|
|
|
|
|
for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); }
|
|
|
|
|
|
dmxf.push(fb);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let sl_branch = std::fs::read_to_string("C:/Users/tonycao/AppData/Local/Temp/opencode/sl_branch_exact.hex")
|
|
|
|
|
|
.unwrap_or_else(|_| String::new());
|
|
|
|
|
|
let mut slb: Vec<u8> = Vec::new();
|
|
|
|
|
|
{
|
|
|
|
|
|
let h = sl_branch.trim();
|
|
|
|
|
|
for i in (0..h.len()).step_by(2) { slb.push(u8::from_str_radix(&h[i..i + 2], 16).unwrap()); }
|
|
|
|
|
|
}
|
|
|
|
|
|
// NFS1K2D GET DEVICE_INFO 探针 (被 MUTE 的设备也须应答 GET — 应答可观测)
|
|
|
|
|
|
let sl_get = std::fs::read_to_string("C:/Users/tonycao/AppData/Local/Temp/opencode/sl_get_exact.hex")
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
let mut slg: Vec<u8> = Vec::new();
|
|
|
|
|
|
{
|
|
|
|
|
|
let h = sl_get.trim();
|
|
|
|
|
|
for i in (0..h.len()).step_by(2) { slg.push(u8::from_str_radix(&h[i..i + 2], 16).unwrap()); }
|
|
|
|
|
|
}
|
|
|
|
|
|
let known: [[u8; 6]; 3] = [
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25],
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00],
|
|
|
|
|
|
[0x08, 0x99, 0x50, 0x00, 0x26, 0xc9],
|
|
|
|
|
|
];
|
|
|
|
|
|
println!("[B] 开始突变序列 (GET→NFS; 每步 1s DMX 背景 + 发送 + 500ms 侦听)");
|
|
|
|
|
|
let mut di = 0usize;
|
|
|
|
|
|
let mutations = ["verbatim对照", "counter=ours", "TN=ours", "qseq=ours", "全突变"];
|
|
|
|
|
|
for round in 0..2 {
|
|
|
|
|
|
for (mi, mname) in mutations.iter().enumerate() {
|
|
|
|
|
|
let seg = Instant::now();
|
|
|
|
|
|
while seg.elapsed() < Duration::from_millis(1000) {
|
|
|
|
|
|
let f = &dmxf[di % dmxf.len()];
|
|
|
|
|
|
di += 1;
|
|
|
|
|
|
let _ = sock.send_to(f, &dst);
|
|
|
|
|
|
if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); }
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(33));
|
|
|
|
|
|
while sock.recv_from(&mut buf).is_ok() {}
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut f = slg.clone();
|
|
|
|
|
|
let ctr = (di as u32).wrapping_add(9000);
|
|
|
|
|
|
match mi {
|
|
|
|
|
|
1 => { f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); }
|
|
|
|
|
|
2 => { f[0x4A] = 0x77; }
|
|
|
|
|
|
3 => { f[0x22] = 0x55; f[0x23] = 0x00; }
|
|
|
|
|
|
4 => {
|
|
|
|
|
|
f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes());
|
|
|
|
|
|
f[0x4A] = 0x78;
|
|
|
|
|
|
f[0x22] = 0x56; f[0x23] = 0x00;
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
let _ = sock.send_to(&f, &dst);
|
|
|
|
|
|
let mut f2 = f.clone();
|
|
|
|
|
|
f2[0x30] = 0x01;
|
|
|
|
|
|
f2[0x49] = f2[0x49].wrapping_sub(1);
|
|
|
|
|
|
let _ = sock.send_to(&f2, &dst);
|
|
|
|
|
|
let mut info = false;
|
|
|
|
|
|
let mut any = 0usize;
|
|
|
|
|
|
let w = Instant::now();
|
|
|
|
|
|
// ★边发 DMX 边侦听 (Butler 仅在 DMX 流持续时吐应答, 停流即饿死)
|
|
|
|
|
|
while w.elapsed() < Duration::from_millis(500) {
|
|
|
|
|
|
let f = &dmxf[di % dmxf.len()];
|
|
|
|
|
|
di += 1;
|
|
|
|
|
|
let _ = sock.send_to(f, &dst);
|
|
|
|
|
|
if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); }
|
|
|
|
|
|
while let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if src.ip().to_string() != target { continue; }
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
any += 1;
|
|
|
|
|
|
if let Some((false, d)) = rdm_region(b) {
|
|
|
|
|
|
if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
|
|
|
|
|
|
info = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(33));
|
|
|
|
|
|
}
|
|
|
|
|
|
println!(" [B] r{} {:<14} → 收帧 {} DEVICE_INFO={}", round, mname, any, info);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let _ = known;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// show: 地址 1-32 炫彩呼吸持续播放 + 并行 RDM 状态轮询 (DEVICE_INFO / DMX 地址)
|
|
|
|
|
|
// 旧的 show 实现已合并到上方 hybrid/show 块;保留结构,条件永假
|
|
|
|
|
|
if false {
|
|
|
|
|
|
let o2: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
|
|
|
|
|
|
let bcast_subnet = format!("{}:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123").to_string() + ".255");
|
|
|
|
|
|
// ---- 冷启动前奏 (默认模式同款; RDM 代理门控, 短前奏 RDM 无应答) ----
|
|
|
|
|
|
let pre_secs: u64 = std::env::var("HOSTDEMO_PRE").ok().and_then(|s| s.parse().ok()).unwrap_or(30);
|
|
|
|
|
|
let disc = load_template("discovery_req");
|
|
|
|
|
|
let euce = load_template("euce");
|
|
|
|
|
|
let pre = Instant::now();
|
|
|
|
|
|
let mut last = Instant::now() - Duration::from_secs(10);
|
|
|
|
|
|
let mut last_euce = Instant::now() - Duration::from_secs(10);
|
|
|
|
|
|
while pre.elapsed() < Duration::from_secs(pre_secs) {
|
|
|
|
|
|
if last.elapsed() >= Duration::from_secs(2) {
|
|
|
|
|
|
let _ = sock.send_to(&disc, "255.255.255.255:31772");
|
|
|
|
|
|
let mut ann = build_fresh_announce(&mut rng);
|
|
|
|
|
|
ann[0x41..0x45].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
|
|
|
|
|
|
let _ = sock.send_to(&ann, &bcast_subnet);
|
|
|
|
|
|
last = Instant::now();
|
|
|
|
|
|
}
|
|
|
|
|
|
if last_euce.elapsed() >= Duration::from_secs(6) {
|
|
|
|
|
|
let _ = sock.send_to(&euce, "255.255.255.255:31772");
|
|
|
|
|
|
last_euce = Instant::now();
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
println!(" [SHOW/前奏] 收帧 len={} src={}", n, src);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] {
|
|
|
|
|
|
let mut b = build_bind52(flag);
|
|
|
|
|
|
b[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
|
|
|
|
|
|
sock.send_to(&b, &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(320));
|
|
|
|
|
|
}
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(1800));
|
|
|
|
|
|
let mut ident = IDENT102.to_vec();
|
|
|
|
|
|
ident[0x1C..0x20].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
|
|
|
|
|
|
sock.send_to(&ident, &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(300));
|
2026-09-11 15:03:09 +08:00
|
|
|
|
// 认领 (死代码块, 保留模板版)
|
2026-09-11 12:03:43 +08:00
|
|
|
|
let nonce = rng.next();
|
|
|
|
|
|
let bc = rng.next();
|
|
|
|
|
|
let mut ch = load_template("claim_h112");
|
|
|
|
|
|
ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
|
|
|
|
|
|
ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes());
|
|
|
|
|
|
ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes());
|
|
|
|
|
|
sock.send_to(&ch, &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(400));
|
|
|
|
|
|
println!("[SHOW] 认领完成 B12=0x{:08x}; 地址1-32 炫彩呼吸 + RDM 状态轮询 (Ctrl+C 停止)", bc);
|
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
|
// SL 实帧 RDM 模板 (自构造帧 Butler 不响应, SL 帧响应 — 实测结论)
|
|
|
|
|
|
let sl_rdm_tpl: Option<Vec<u8>> = std::fs::read_to_string("C:/Users/tonycao/AppData/Local/Temp/opencode/sl_get_exact.hex").ok().map(|h| {
|
|
|
|
|
|
let h = h.trim();
|
|
|
|
|
|
let mut v = Vec::new();
|
|
|
|
|
|
for i in (0..h.len()).step_by(2) { v.push(u8::from_str_radix(&h[i..i + 2], 16).unwrap()); }
|
|
|
|
|
|
v
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let disc = load_template("discovery_req");
|
|
|
|
|
|
let mut ann = load_template("announce");
|
|
|
|
|
|
ann[0x41..0x45].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
|
|
|
|
|
|
let known: [[u8; 6]; 3] = [
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25],
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00],
|
|
|
|
|
|
[0x08, 0x99, 0x50, 0x00, 0x26, 0xc9],
|
|
|
|
|
|
];
|
|
|
|
|
|
// 状态表: UID → (DMX地址, 通道数)
|
|
|
|
|
|
let mut state: Vec<([u8; 6], u16, u16)> = Vec::new();
|
|
|
|
|
|
let mut di = 0usize;
|
|
|
|
|
|
let mut last_bcast = Instant::now() - Duration::from_secs(10);
|
|
|
|
|
|
let mut last_hb = Instant::now() - Duration::from_secs(10);
|
|
|
|
|
|
let mut last_rdm = Instant::now() - Duration::from_secs(10);
|
|
|
|
|
|
let mut poll_idx = 0usize;
|
|
|
|
|
|
let mut addr_req_pending = false;
|
|
|
|
|
|
let mut last_status = Instant::now() - Duration::from_secs(100);
|
|
|
|
|
|
let mut resp_n: u64 = 0;
|
|
|
|
|
|
loop {
|
|
|
|
|
|
// 广播 (RDM 代理开关)
|
|
|
|
|
|
if last_bcast.elapsed() >= Duration::from_millis(2500) {
|
|
|
|
|
|
let _ = sock.send_to(&disc, "255.255.255.255:31772");
|
|
|
|
|
|
let _ = sock.send_to(&ann, &format!("{}:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123").to_string() + ".255"));
|
|
|
|
|
|
last_bcast = Instant::now();
|
|
|
|
|
|
}
|
|
|
|
|
|
// 心跳
|
|
|
|
|
|
if last_hb.elapsed() >= Duration::from_secs(1) {
|
|
|
|
|
|
let _ = sock.send_to(&load_template("heartbeat"), &dst);
|
|
|
|
|
|
last_hb = Instant::now();
|
|
|
|
|
|
}
|
|
|
|
|
|
// RDM 轮询: 每 700ms 一步, 轮转 3 台 × (DEVICE_INFO / START_ADDRESS)
|
|
|
|
|
|
if last_rdm.elapsed() >= Duration::from_millis(700) {
|
|
|
|
|
|
last_rdm = Instant::now();
|
|
|
|
|
|
let uid = known[poll_idx % 3];
|
|
|
|
|
|
let (pid, cc) = if addr_req_pending { (0x00F0u16, 0x20u8) } else { (0x0060, 0x20) };
|
|
|
|
|
|
// RDM 请求用 SL 实帧模板 (实测: 自构造帧 Butler 不响应, SL 帧响应)
|
|
|
|
|
|
let mut f = match &sl_rdm_tpl {
|
|
|
|
|
|
Some(t) => {
|
|
|
|
|
|
let mut t = t.clone();
|
|
|
|
|
|
t[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());
|
|
|
|
|
|
t[0x3D] = 24; // msgLen (PDL=0)
|
|
|
|
|
|
t[0x3E..0x44].copy_from_slice(&uid);
|
|
|
|
|
|
t[0x4A] = (poll_idx * 13 + 1) as u8;
|
|
|
|
|
|
t[0x4F] = cc;
|
|
|
|
|
|
t[0x50] = (pid >> 8) as u8;
|
|
|
|
|
|
t[0x51] = (pid & 0xFF) as u8;
|
|
|
|
|
|
t[0x52] = 0;
|
|
|
|
|
|
let mut sum: u16 = 0xCC;
|
|
|
|
|
|
for i in 0x3C..0x3C + 24 { sum = sum.wrapping_add(t[i] as u16); }
|
|
|
|
|
|
t[0x5F] = (sum >> 8) as u8;
|
|
|
|
|
|
t[0x60] = (sum & 0xFF) as u8;
|
|
|
|
|
|
t
|
|
|
|
|
|
}
|
|
|
|
|
|
None => build_rdm_frame(&uid, (poll_idx * 13 + 1) as u8, poll_idx as u16, cc, pid, &[]),
|
|
|
|
|
|
};
|
|
|
|
|
|
let mut f2 = f.clone();
|
|
|
|
|
|
f2[0x30] = 0x01;
|
|
|
|
|
|
f2[0x49] = f2[0x49].wrapping_sub(1);
|
|
|
|
|
|
let _ = sock.send_to(&f, &dst);
|
|
|
|
|
|
let _ = sock.send_to(&f2, &dst);
|
|
|
|
|
|
poll_idx += 1;
|
|
|
|
|
|
if poll_idx % 3 == 0 { addr_req_pending = !addr_req_pending; }
|
|
|
|
|
|
}
|
|
|
|
|
|
// 收包: 解析 DEVICE_INFO / START_ADDRESS, 报告变更
|
|
|
|
|
|
while let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if src.ip().to_string() != target { continue; }
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
if let Some((false, d)) = rdm_region(b) {
|
|
|
|
|
|
if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
|
|
|
|
|
|
let uid = [d[9], d[10], d[11], d[12], d[13], d[14]];
|
|
|
|
|
|
let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]);
|
|
|
|
|
|
let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]);
|
|
|
|
|
|
match state.iter_mut().find(|(u, _, _)| *u == uid) {
|
|
|
|
|
|
Some(e) => {
|
|
|
|
|
|
if e.1 != a || e.2 != fp {
|
|
|
|
|
|
println!(" [SHOW] ★{} 状态变更: 地址 {}→{} 通道 {}→{}", uid_s(&uid), e.1, a, e.2, fp);
|
|
|
|
|
|
e.1 = a; e.2 = fp;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
None => {
|
|
|
|
|
|
state.push((uid, a, fp));
|
|
|
|
|
|
println!(" [SHOW] ★设备注册: {} 地址={} 通道={} 传感器={}", uid_s(&uid), a, fp, d[0x18 + 18]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0xF0] {
|
|
|
|
|
|
let uid = [d[9], d[10], d[11], d[12], d[13], d[14]];
|
|
|
|
|
|
let a = u16::from_be_bytes([d[0x18], d[0x19]]);
|
|
|
|
|
|
if let Some(e) = state.iter_mut().find(|(u, _, _)| *u == uid) {
|
|
|
|
|
|
if e.1 != a {
|
|
|
|
|
|
println!(" [SHOW] ★{} START_ADDRESS 变更: {}→{}", uid_s(&uid), e.1, a);
|
|
|
|
|
|
e.1 = a;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 每 10s 打印一次状态汇总
|
|
|
|
|
|
if last_status.elapsed() >= Duration::from_secs(10) {
|
|
|
|
|
|
last_status = Instant::now();
|
|
|
|
|
|
let lines: Vec<String> = state.iter().map(|(u, a, fp)| format!("{} @DMX{} ({}ch)", uid_s(u), a, fp)).collect();
|
|
|
|
|
|
println!("[SHOW] 状态: {} 台 | {} | 收帧{}", state.len(), lines.join(" | "), resp_n);
|
|
|
|
|
|
}
|
|
|
|
|
|
// DMX @30fps: 地址 1-32 彩虹波 × 呼吸包络
|
|
|
|
|
|
let t = start.elapsed().as_secs_f32();
|
|
|
|
|
|
di += 1;
|
|
|
|
|
|
let mut p1 = [0u8; 512];
|
|
|
|
|
|
let breathe = (t * 0.9).sin() * 0.5 + 0.5; // 全局呼吸包络 0..1
|
|
|
|
|
|
for i in 0..32usize {
|
|
|
|
|
|
let wave = ((t * 1.2 + i as f32 * 0.55).sin() * 0.5 + 0.5); // 行波
|
|
|
|
|
|
p1[i] = (wave * breathe * 255.0) as u8;
|
|
|
|
|
|
}
|
|
|
|
|
|
let rr1 = rng.next();
|
|
|
|
|
|
let rr2 = rng.next();
|
|
|
|
|
|
let mut f = load_template("dmx_a");
|
|
|
|
|
|
f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());
|
|
|
|
|
|
f[0x22..0x26].copy_from_slice(&rr1.to_le_bytes());
|
|
|
|
|
|
f[0x26..0x2A].copy_from_slice(&rr2.to_le_bytes());
|
|
|
|
|
|
f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, rr1, bc));
|
|
|
|
|
|
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], rr2, bc));
|
|
|
|
|
|
let _ = sock.send_to(&f, &dst);
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(33));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut b12 = rng.next();
|
|
|
|
|
|
let mut claim_ok = false;
|
|
|
|
|
|
|
|
|
|
|
|
// claimonly/quick: 最小认领 (announce144 → 47B → AECH) — 实测成功配方, 无绑定/宣告/前奏
|
|
|
|
|
|
let hd_mode = std::env::var("HOSTDEMO_MODE").unwrap_or_default();
|
|
|
|
|
|
if hd_mode.starts_with("claim") || hd_mode == "quick" {
|
|
|
|
|
|
let o2: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
|
|
|
|
|
|
let full = hd_mode == "claimfull";
|
|
|
|
|
|
if full {
|
|
|
|
|
|
// sub=07 绑定帧 ×4 (ffff→0000→ffff→0000, 0.32s 间隔)
|
|
|
|
|
|
for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] {
|
|
|
|
|
|
let mut b = build_bind52(flag);
|
|
|
|
|
|
b[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
|
|
|
|
|
|
sock.send_to(&b, &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(320));
|
|
|
|
|
|
}
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(1500));
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut ident = IDENT102.to_vec();
|
|
|
|
|
|
ident[0x1C..0x20].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
|
|
|
|
|
|
// runs1-3 成功配方: 仅 announce144 (102B 身份帧) 单播一次, 无绑定/EUCE/宣告
|
|
|
|
|
|
let ident144 = if full { ident } else { load_template("announce144") };
|
|
|
|
|
|
sock.send_to(&ident144, &dst).unwrap();
|
|
|
|
|
|
let mut reg = false;
|
|
|
|
|
|
let w = Instant::now();
|
|
|
|
|
|
while w.elapsed() < Duration::from_millis(400) {
|
|
|
|
|
|
if let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if src.ip().to_string() == target && n == 47 { reg = true; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let nonce = rng.next();
|
|
|
|
|
|
let b12c = rng.next();
|
|
|
|
|
|
let mut ch = load_template("claim_h112");
|
|
|
|
|
|
ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
|
|
|
|
|
|
ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes());
|
|
|
|
|
|
ch[0x8C..0x90].copy_from_slice(&b12c.to_le_bytes());
|
feat: decode remaining frame fields (claim/AERE/announce/discovery-resp/EUCE/events)
- claim 552B: full layout; 240B opaque blocks proven UNCHECKED by Butler
(CLAIM_ZERO=1 ablation: zeroed blocks -> AERE still PASS) -> claim can be
built from scratch with zeros
- AERE 552B: Butler IP/MAC/netmask, fixture UID@0x33, slow counter@0xB1/0xB5,
resp@0xC0; only resp+counter vary across captures
- announce 644B: session GUID, UTF-16BE show-file path, full timestamp
@0x239 (y/mo/d/h/mi/s/ms; live sample decodes to 22:52:50.992 matching SL
session), ephemeral port@0x25C, enable flags
- discovery-resp 217B: device name, serial blob, network block, config block
- EUCE 540B: 'EUCE' magic, version 5, username+computername+GUID, host MAC
- event_b: type=4 1084B RDM-uplink frame identified (RDM response @0x3C)
- analyze: new 'xor' subcommand with periodicity analysis
2026-09-11 14:49:39 +08:00
|
|
|
|
// CLAIM_ZERO=1: 清零不透明区 (0x2C-0x87, 0x90-0x123), 消融验证 Butler 是否校验
|
|
|
|
|
|
if std::env::var("CLAIM_ZERO").unwrap_or_default() == "1" {
|
|
|
|
|
|
for i in 0x2C..0x88 { ch[i] = 0; }
|
|
|
|
|
|
for i in 0x90..0x124 { ch[i] = 0; }
|
|
|
|
|
|
println!("CLAIM_ZERO: 不透明区已清零 (240B)");
|
|
|
|
|
|
}
|
2026-09-11 12:03:43 +08:00
|
|
|
|
sock.send_to(&ch, &dst).unwrap();
|
|
|
|
|
|
let w2 = Instant::now();
|
|
|
|
|
|
while w2.elapsed() < Duration::from_millis(700) {
|
|
|
|
|
|
if let Ok((n, _)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
if n > 0xC4 && b.windows(4).any(|w| w == b"AERE") {
|
|
|
|
|
|
let resp = g32(b, 0xC0);
|
|
|
|
|
|
println!("CLAIM_PROBE reg89={} AERE resp=0x{:08x} G=0x{:08x} {}", reg, resp, agios_g(nonce),
|
|
|
|
|
|
if resp == agios_g(nonce) { "PASS" } else { "FAIL" });
|
|
|
|
|
|
if resp == agios_g(nonce) {
|
|
|
|
|
|
// quick 模式: 认领成功后直通 RDM+DMX 阶段 (跳过绑定/宣告/前奏 — 实测有毒)
|
|
|
|
|
|
if hd_mode == "quick" {
|
|
|
|
|
|
b12 = b12c;
|
|
|
|
|
|
claim_ok = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !claim_ok { return; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !claim_ok {
|
|
|
|
|
|
println!("CLAIM_PROBE reg89={} AERE=none FAIL", reg);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// takeover: 跳过认领, 用 SL 上次会话的 B12 续发 DMX+心跳, 探会话是否可直接延续
|
|
|
|
|
|
if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "takeover" {
|
|
|
|
|
|
let b12_old: u32 = 0x4366937a; // sym_startup @101.278 AECH 的 B12@0x8C (SL 22:52 会话)
|
|
|
|
|
|
println!("[P] takeover: B12=0x{:08x} 续发 15s DMX(ch101 ramp)+心跳+RDM GET, 侦听回包", b12_old);
|
|
|
|
|
|
let tpl = load_template("dmx_a");
|
|
|
|
|
|
let hb = load_template("heartbeat");
|
|
|
|
|
|
let known: [u8; 6] = [0x01, 0x07, 0x34, 0x67, 0x01, 0x25]; // NFS1K2D
|
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
|
let mut seq: u32 = 5000; // 帧序号跳开, 避免与 SL 旧计数冲突
|
|
|
|
|
|
let mut last_hb = Instant::now() - Duration::from_secs(10);
|
|
|
|
|
|
let mut rdm_done = false;
|
|
|
|
|
|
while start.elapsed() < Duration::from_secs(15) {
|
|
|
|
|
|
if last_hb.elapsed() >= Duration::from_secs(6) {
|
|
|
|
|
|
let _ = sock.send_to(&hb, &dst);
|
|
|
|
|
|
last_hb = Instant::now();
|
|
|
|
|
|
println!(" [P] 心跳已发");
|
|
|
|
|
|
}
|
|
|
|
|
|
if !rdm_done && start.elapsed() > Duration::from_secs(5) {
|
|
|
|
|
|
rdm_done = true;
|
|
|
|
|
|
let f = build_rdm_frame(&known, 1, 1, 0x20, 0x0060, &[]); // GET DEVICE_INFO
|
|
|
|
|
|
sock.send_to(&f, &dst).unwrap();
|
|
|
|
|
|
println!(" [P] RDM GET DEVICE_INFO 已发");
|
|
|
|
|
|
}
|
|
|
|
|
|
let t = start.elapsed().as_secs_f32();
|
|
|
|
|
|
let mut p1 = [0u8; 512];
|
|
|
|
|
|
p1[100] = (((t * 0.8).sin() * 0.5 + 0.5) * 255.0) as u8;
|
|
|
|
|
|
let p2 = [0u8; 512];
|
|
|
|
|
|
let r1 = rng.next();
|
|
|
|
|
|
let r2 = rng.next();
|
|
|
|
|
|
let mut f = tpl.clone();
|
|
|
|
|
|
f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes());
|
|
|
|
|
|
f[0x2E] = 2; f[0x2F] = 0;
|
|
|
|
|
|
f[0x22..0x26].copy_from_slice(&r1.to_le_bytes());
|
|
|
|
|
|
f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes());
|
|
|
|
|
|
f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, b12_old));
|
|
|
|
|
|
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&p2, r2, b12_old));
|
|
|
|
|
|
let _ = sock.send_to(&f, &dst);
|
|
|
|
|
|
seq += 1;
|
|
|
|
|
|
while let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
println!(" [P] ← len={} src={} head={}", n, src, hexs(&b[..std::cmp::min(16, n)]));
|
|
|
|
|
|
}
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(33));
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 2) 宣告: 真主机用 644B type=1 广播宣告 (announce 模板), 主机 IP@0x41 LE;
|
|
|
|
|
|
// 再发 102B type=2 身份帧 (cold2 实测: 紧邻 AECH 之前), IP@0x1C LE ----
|
|
|
|
|
|
let mode = std::env::var("HOSTDEMO_MODE").unwrap_or_default();
|
|
|
|
|
|
let o: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
|
|
|
|
|
|
let bcast = format!("{}.255:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123"));
|
|
|
|
|
|
if mode != "skip2" && mode != "noclaim" {
|
|
|
|
|
|
// 冷启动广播前奏 (cold2=38s, SymphoLight 实测 2.5min 成功; 10s 短前奏被拒!)
|
|
|
|
|
|
// ★门控假说: Butler 须收到 ~13+ 轮 discovery/announce 才武装认领应答
|
|
|
|
|
|
let pre_secs: u64 = std::env::var("HOSTDEMO_PRE").ok().and_then(|s| s.parse().ok()).unwrap_or(45);
|
|
|
|
|
|
// ★宣告帧用 announce_live 模板 + 新鲜时间戳/随机会话 ID (陈旧宣告 = 僵尸主机, 认领被无视)
|
|
|
|
|
|
let euce = load_template("euce"); // 540B, 无主机相关字段
|
|
|
|
|
|
let pre = Instant::now();
|
|
|
|
|
|
let mut queries_dbg = 0;
|
|
|
|
|
|
let mut last = Instant::now() - Duration::from_secs(10);
|
|
|
|
|
|
let mut last_euce = Instant::now() - Duration::from_secs(10);
|
|
|
|
|
|
while pre.elapsed() < Duration::from_secs(pre_secs) {
|
|
|
|
|
|
if last.elapsed() >= Duration::from_secs(2) {
|
|
|
|
|
|
let _ = sock.send_to(&disc, "255.255.255.255:31772");
|
|
|
|
|
|
let mut ann = build_fresh_announce(&mut rng);
|
|
|
|
|
|
ann[0x41..0x45].copy_from_slice(&[o[3], o[2], o[1], o[0]]); // 主机IP逆序字节@0x41
|
|
|
|
|
|
if queries_dbg == 0 {
|
|
|
|
|
|
queries_dbg = 1;
|
|
|
|
|
|
let live = load_template("announce_live");
|
|
|
|
|
|
println!(" [dbg] ann timestamp区: ours={}", hexs(&ann[0x230..0x245]));
|
|
|
|
|
|
println!(" [dbg] ann timestamp区: live={}", hexs(&live[0x230..0x245]));
|
|
|
|
|
|
println!(" [dbg] ann sessid区: ours={}", hexs(&ann[0x219..0x226]));
|
|
|
|
|
|
println!(" [dbg] ann sessid区: live={}", hexs(&live[0x219..0x226]));
|
|
|
|
|
|
let diffs: Vec<usize> = (0..ann.len()).filter(|&i| ann[i] != live[i]).collect();
|
|
|
|
|
|
println!(" [dbg] ann 与 live 模板差异偏移 ({}处): {:?}", diffs.len(), diffs);
|
|
|
|
|
|
}
|
|
|
|
|
|
let _ = sock.send_to(&ann, &bcast);
|
|
|
|
|
|
last = Instant::now();
|
|
|
|
|
|
}
|
|
|
|
|
|
if last_euce.elapsed() >= Duration::from_secs(6) {
|
|
|
|
|
|
// ★EUCE 每 6s (SymphoLight 启动期实测节奏, 30s 节奏认领被拒)
|
|
|
|
|
|
let _ = sock.send_to(&euce, "255.255.255.255:31772");
|
|
|
|
|
|
last_euce = Instant::now();
|
|
|
|
|
|
}
|
|
|
|
|
|
// 排空 (217B 发现应答 / 47B 注册等)
|
|
|
|
|
|
if let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
println!(" [dbg] 前奏期收帧 len={} src={}", n, src);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut ident = IDENT102.to_vec();
|
|
|
|
|
|
ident[0x1C..0x20].copy_from_slice(&[o[3], o[2], o[1], o[0]]);
|
|
|
|
|
|
// ★sub=07 绑定帧 ×4: ffff→0000→ffff→0000 (间隔 ~0.32s, sym_startup @98.8-99.5 实测)
|
|
|
|
|
|
// 1.8s 后发 ident
|
|
|
|
|
|
for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] {
|
|
|
|
|
|
let mut b = build_bind52(flag);
|
|
|
|
|
|
b[0x1A..0x1E].copy_from_slice(&[o[3], o[2], o[1], o[0]]);
|
|
|
|
|
|
sock.send_to(&b, &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(320));
|
|
|
|
|
|
}
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(1800));
|
|
|
|
|
|
let mut ident = IDENT102.to_vec();
|
|
|
|
|
|
ident[0x1C..0x20].copy_from_slice(&[o[3], o[2], o[1], o[0]]);
|
|
|
|
|
|
sock.send_to(&ident, &dst).unwrap();
|
|
|
|
|
|
println!("[2] 冷启动前奏完成 ({}s 广播 + 绑定帧×4 + 身份帧)", pre_secs);
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut reg_ok = false;
|
|
|
|
|
|
for _ in 0..10 {
|
|
|
|
|
|
if let Ok((n, _)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if n == 89 { reg_ok = true; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
println!("[2] 宣告 → 注册应答: {}", if reg_ok { "OK (89B)" } else { "未收到 (继续)" });
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 3) AECH 认领 ×3 (quick 模式已在上面认领成功; noclaim 跳过) ----
|
|
|
|
|
|
if !claim_ok {
|
|
|
|
|
|
b12 = rng.next();
|
|
|
|
|
|
if mode == "noclaim" {
|
|
|
|
|
|
println!("[3] noclaim 模式: 跳过 AECH, 直接以 RDM GET 探测连接态");
|
|
|
|
|
|
claim_ok = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
for round in 0..3 {
|
|
|
|
|
|
if mode == "noclaim" { break; }
|
|
|
|
|
|
let nonce = rng.next();
|
|
|
|
|
|
let mut ch = load_template("claim_h112");
|
|
|
|
|
|
ch[0x1A..0x1E].copy_from_slice(&[o[3], o[2], o[1], o[0]]);
|
|
|
|
|
|
ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes());
|
|
|
|
|
|
ch[0x8C..0x90].copy_from_slice(&b12.to_le_bytes());
|
|
|
|
|
|
sock.send_to(&ch, &dst).unwrap();
|
|
|
|
|
|
for _ in 0..10 {
|
|
|
|
|
|
if let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
println!(" [dbg] AECH等待中收帧 len={} src={} magic={:?}", n, src, &b[0..4.min(n)]);
|
|
|
|
|
|
if n > 0xC4 && b.windows(4).any(|w| w == b"AERE") {
|
|
|
|
|
|
let resp = g32(b, 0xC0);
|
|
|
|
|
|
let want = agios_g(nonce);
|
|
|
|
|
|
println!("[3] AECH 第{}轮: resp=0x{:08x} G=0x{:08x} {}", round + 1, resp, want,
|
|
|
|
|
|
if resp == want { "★PASS" } else { "FAIL" });
|
|
|
|
|
|
if resp == want { claim_ok = true; }
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !claim_ok { println!("[3] 认领失败 (真机只应答源 123.113 的 AECH), 终止"); return; }
|
|
|
|
|
|
println!(" 认领通过, B12=0x{:08x}", b12);
|
|
|
|
|
|
|
|
|
|
|
|
// quick 模式: 密钥广播 + 统一会话环 — DMX 持续流 (cold2 实证: RDM 代理仅在 DMX 流活跃时工作)
|
|
|
|
|
|
if hd_mode == "quick" {
|
|
|
|
|
|
println!("[K] 1947 许可密钥广播 4s...");
|
|
|
|
|
|
send_key_burst(&sock);
|
|
|
|
|
|
session_loop(&sock, &dst, target, &mut rng, b12, secs);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// noclaim 连接态探测: 直接 GET DEVICE_INFO 已知 3 灯具, 验证 Butler 是否仍视我们为主机
|
|
|
|
|
|
if mode == "noclaim" {
|
|
|
|
|
|
let known: [[u8; 6]; 3] = [
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2D DMX101
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680S DMX112
|
|
|
|
|
|
[0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32
|
|
|
|
|
|
];
|
|
|
|
|
|
let mut tn0: u8 = rng.byte();
|
|
|
|
|
|
for (i, uid) in known.iter().enumerate() {
|
|
|
|
|
|
tn0 = tn0.wrapping_add(1);
|
|
|
|
|
|
let f = build_rdm_frame(uid, tn0, (i + 1) as u16, 0x20, 0x0060, &[]);
|
|
|
|
|
|
sock.send_to(&f, &dst).unwrap();
|
|
|
|
|
|
let w = Instant::now();
|
|
|
|
|
|
let mut got = false;
|
|
|
|
|
|
while w.elapsed() < Duration::from_millis(500) {
|
|
|
|
|
|
if let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if src.ip().to_string() != target { continue; }
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
println!(" [dbg] 收帧 len={} 0x18={:02x}{:02x}", n, b.get(0x19).copied().unwrap_or(0), b.get(0x18).copied().unwrap_or(0));
|
|
|
|
|
|
if let Some((false, d)) = rdm_region(b) {
|
|
|
|
|
|
if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
|
|
|
|
|
|
let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]);
|
|
|
|
|
|
println!(" ★ 连接态确认! UID={} DMX地址={}", uid_s(uid), a);
|
|
|
|
|
|
got = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !got { println!(" UID={} 无应答", uid_s(uid)); }
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 4) RDM 发现 ----
|
|
|
|
|
|
let mut tn: u8 = rng.byte();
|
|
|
|
|
|
let mut qseq: u16 = 1;
|
|
|
|
|
|
let mut fixtures: Vec<[u8; 6]> = Vec::new();
|
|
|
|
|
|
{
|
|
|
|
|
|
// 4a) 先直查已知 UID 验证 RDM 代理通道 (灯具可能仍处 SL 会话留下的 MUTE 态)
|
|
|
|
|
|
let known: [[u8; 6]; 3] = [
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2D DMX101
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680S DMX112
|
|
|
|
|
|
[0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32
|
|
|
|
|
|
];
|
|
|
|
|
|
println!("[4a] 直查已知 UID (GET DEVICE_INFO):");
|
|
|
|
|
|
for uid in &known {
|
|
|
|
|
|
tn = tn.wrapping_add(1);
|
|
|
|
|
|
let f = build_rdm_frame(uid, tn, qseq, 0x20, 0x0060, &[]);
|
|
|
|
|
|
qseq += 1;
|
|
|
|
|
|
sock.send_to(&f, &dst).unwrap();
|
|
|
|
|
|
let w = Instant::now();
|
|
|
|
|
|
let mut ok = false;
|
|
|
|
|
|
while w.elapsed() < Duration::from_millis(400) {
|
|
|
|
|
|
if let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if src.ip().to_string() != target { continue; }
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
if let Some((false, d)) = rdm_region(b) {
|
|
|
|
|
|
if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
|
|
|
|
|
|
let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]);
|
|
|
|
|
|
let footprint = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]);
|
|
|
|
|
|
println!(" ★ {} → DMX地址 {} / {}通道", uid_s(uid), a, footprint);
|
|
|
|
|
|
if !fixtures.contains(uid) { fixtures.push(*uid); }
|
|
|
|
|
|
ok = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !ok { println!(" {} → 无应答", uid_s(uid)); }
|
|
|
|
|
|
}
|
|
|
|
|
|
// 先广播 DISC_UN_MUTE (PID=0003): 稳态下主机已把全部灯具 MUTE,
|
|
|
|
|
|
// 被 MUTE 的灯具不应答 UNIQUE_BRANCH → 须先解除静音才能重新发现
|
|
|
|
|
|
tn = tn.wrapping_add(1);
|
|
|
|
|
|
let um = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0003, &[]);
|
|
|
|
|
|
qseq += 1;
|
|
|
|
|
|
sock.send_to(&um, &dst).unwrap();
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(80));
|
|
|
|
|
|
println!("[4] 已广播 DISC_UN_MUTE, 开始 UNIQUE_BRANCH 二分遍历");
|
|
|
|
|
|
let mut stack: Vec<(u64, u64)> = vec![(0, 0xFFFF_FFFF_FFFF)];
|
|
|
|
|
|
let mut queries = 0;
|
|
|
|
|
|
let walk_start = Instant::now();
|
|
|
|
|
|
while let Some((lo, hi)) = stack.pop() {
|
|
|
|
|
|
if queries >= 400 || walk_start.elapsed() > Duration::from_secs(120) { println!(" (发现遍历到达上限)"); break; }
|
|
|
|
|
|
let mut pd = Vec::with_capacity(12);
|
|
|
|
|
|
pd.extend_from_slice(&u64_uid(lo));
|
|
|
|
|
|
pd.extend_from_slice(&u64_uid(hi));
|
|
|
|
|
|
// 真机实测: 首个应答延迟可达 ~1.4s, 真主机对同一分支连发多次 → 每分支最多 4 次尝试 × 250ms 窗口
|
|
|
|
|
|
let mut found: Vec<[u8; 6]> = Vec::new();
|
|
|
|
|
|
let mut collision = false;
|
|
|
|
|
|
for attempt in 0..4 {
|
|
|
|
|
|
queries += 1;
|
|
|
|
|
|
tn = tn.wrapping_add(1);
|
|
|
|
|
|
let f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0001, &pd); // DISC_UNIQUE_BRANCH
|
|
|
|
|
|
qseq += 1;
|
|
|
|
|
|
if queries == 1 {
|
|
|
|
|
|
let t = load_template("rdm_disc_req");
|
|
|
|
|
|
let diffs: Vec<usize> = (0x2A..0x62).filter(|&i| f[i] != t[i]).collect();
|
|
|
|
|
|
println!(" [dbg] 首帧与模板差异偏移: {:?} (应仅 qseq/TN/srcUID末字节/PD/checksum)", diffs);
|
|
|
|
|
|
}
|
|
|
|
|
|
sock.send_to(&f, &dst).unwrap();
|
|
|
|
|
|
let w = Instant::now();
|
|
|
|
|
|
while w.elapsed() < Duration::from_millis(250) {
|
|
|
|
|
|
if let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
if src.ip().to_string() != target { continue; }
|
|
|
|
|
|
if let Some((is_pulse, d)) = rdm_region(b) {
|
|
|
|
|
|
if is_pulse {
|
|
|
|
|
|
match decode_pulse(d) {
|
|
|
|
|
|
Some(uid) => { if !found.contains(&uid) { found.push(uid); } }
|
|
|
|
|
|
None => collision = true,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !found.is_empty() || collision { break; }
|
|
|
|
|
|
let _ = attempt;
|
|
|
|
|
|
}
|
|
|
|
|
|
if collision {
|
|
|
|
|
|
let mid = lo + (hi - lo) / 2;
|
|
|
|
|
|
stack.push((mid + 1, hi));
|
|
|
|
|
|
stack.push((lo, mid));
|
|
|
|
|
|
} else if found.len() == 1 {
|
|
|
|
|
|
let uid = found[0];
|
|
|
|
|
|
if found_valid(&uid) {
|
|
|
|
|
|
println!(" [发现] UID={} (深度 {} 次查询后)", uid_s(&uid), queries);
|
|
|
|
|
|
// DISC_MUTE (PID=0002) → 70B 参数 ACK
|
|
|
|
|
|
tn = tn.wrapping_add(1);
|
|
|
|
|
|
let f2 = build_rdm_frame(&uid, tn, qseq, 0x10, 0x0002, &[]);
|
|
|
|
|
|
qseq += 1;
|
|
|
|
|
|
sock.send_to(&f2, &dst).unwrap();
|
|
|
|
|
|
let w2 = Instant::now();
|
|
|
|
|
|
let mut acked = false;
|
|
|
|
|
|
while w2.elapsed() < Duration::from_millis(400) {
|
|
|
|
|
|
if let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if src.ip().to_string() != target { continue; }
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
if let Some((false, d)) = rdm_region(b) {
|
|
|
|
|
|
if d.len() >= 0x18 && d[0] == 0xCC && d[0x14] == 0x11 && d[0x15..0x17] == [0x00, 0x02] {
|
|
|
|
|
|
acked = true; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
println!(" MUTE(0002) ACK: {}", if acked { "OK" } else { "未收到" });
|
|
|
|
|
|
fixtures.push(uid);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if found.len() > 1 {
|
|
|
|
|
|
let mid = lo + (hi - lo) / 2;
|
|
|
|
|
|
stack.push((mid + 1, hi));
|
|
|
|
|
|
stack.push((lo, mid));
|
|
|
|
|
|
}
|
|
|
|
|
|
// found==0: 空分支
|
|
|
|
|
|
}
|
|
|
|
|
|
println!("[4] RDM 发现完成: {} 台灯具, {} 次查询, {:.1}s", fixtures.len(), queries, walk_start.elapsed().as_secs_f32());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 4b) 读参数: DEVICE_INFO / DMX_START_ADDRESS / DEVICE_LABEL ----
|
|
|
|
|
|
let mut addr_list: Vec<u16> = Vec::new();
|
|
|
|
|
|
for uid in &fixtures {
|
|
|
|
|
|
tn = tn.wrapping_add(1);
|
|
|
|
|
|
let f = build_rdm_frame(uid, tn, qseq, 0x20, 0x0060, &[]); // GET DEVICE_INFO
|
|
|
|
|
|
qseq += 1;
|
|
|
|
|
|
sock.send_to(&f, &dst).unwrap();
|
|
|
|
|
|
let mut dmx_addr: Option<u16> = None;
|
|
|
|
|
|
let mut footprint = 0u16;
|
|
|
|
|
|
let mut sensors = 0u8;
|
|
|
|
|
|
let w = Instant::now();
|
|
|
|
|
|
while w.elapsed() < Duration::from_millis(400) {
|
|
|
|
|
|
if let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if src.ip().to_string() != target { continue; }
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
if let Some((false, d)) = rdm_region(b) {
|
|
|
|
|
|
// d: CC@0 ... CC字段@0x14, PID@0x15-16, PDL@0x17, PD@0x18
|
|
|
|
|
|
if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x14] == 0x21 && d[0x15..0x17] == [0x00, 0x60] {
|
|
|
|
|
|
let pd = &d[0x18..0x18 + 19];
|
|
|
|
|
|
footprint = u16::from_be_bytes([pd[10], pd[11]]);
|
|
|
|
|
|
dmx_addr = Some(u16::from_be_bytes([pd[14], pd[15]]));
|
|
|
|
|
|
sensors = pd[18];
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// GET DMX_START_ADDRESS 复核
|
|
|
|
|
|
tn = tn.wrapping_add(1);
|
|
|
|
|
|
let f2 = build_rdm_frame(uid, tn, qseq, 0x20, 0x00F0, &[]);
|
|
|
|
|
|
qseq += 1;
|
|
|
|
|
|
sock.send_to(&f2, &dst).unwrap();
|
|
|
|
|
|
let w2 = Instant::now();
|
|
|
|
|
|
while w2.elapsed() < Duration::from_millis(400) {
|
|
|
|
|
|
if let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if src.ip().to_string() != target { continue; }
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
if let Some((false, d)) = rdm_region(b) {
|
|
|
|
|
|
if d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x14] == 0x21 && d[0x15..0x17] == [0x00, 0xF0] {
|
|
|
|
|
|
let a = u16::from_be_bytes([d[0x18], d[0x19]]);
|
|
|
|
|
|
if dmx_addr.is_none() { dmx_addr = Some(a); }
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
match dmx_addr {
|
|
|
|
|
|
Some(a) => {
|
|
|
|
|
|
println!(" 灯具 {} → DMX 地址 {} (占用 {} 通道, {} 传感器)", uid_s(uid), a, footprint, sensors);
|
|
|
|
|
|
addr_list.push(a);
|
|
|
|
|
|
}
|
|
|
|
|
|
None => println!(" 灯具 {} → 参数读取超时", uid_s(uid)),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 5) DMX 控制: 呼吸图案 (按发现的灯具地址; 无发现则用已知 101/112/1) ----
|
|
|
|
|
|
if secs == 0 { println!("[5] secs=0, 跳过 DMX 控制"); return; }
|
|
|
|
|
|
if addr_list.is_empty() { addr_list = vec![101, 112, 1]; println!("[5] 无发现结果, 用已知地址 101/112/1"); }
|
|
|
|
|
|
println!("[5] DMX 控制 {}s @30fps: 呼吸图案 @地址 {:?} (zPprv0TXRRW 加密, B12=0x{:08x})", secs, addr_list, b12);
|
|
|
|
|
|
println!(" ⚠ SymphoLight 若同时运行会插黑场帧造成闪烁; 关其输出可得纯净效果");
|
|
|
|
|
|
let tpl = load_template("dmx_a");
|
|
|
|
|
|
let hb = load_template("heartbeat"); // 36B type=02 sub=03 心跳 (会话保活, 每 ~6s 单播)
|
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
|
let mut seq: u32 = 1;
|
|
|
|
|
|
let mut sent = 0u64;
|
|
|
|
|
|
let mut last_hb = Instant::now();
|
|
|
|
|
|
while start.elapsed() < Duration::from_secs(secs) {
|
|
|
|
|
|
if last_hb.elapsed() >= Duration::from_secs(6) {
|
|
|
|
|
|
let _ = sock.send_to(&hb, &dst);
|
|
|
|
|
|
last_hb = Instant::now();
|
|
|
|
|
|
}
|
|
|
|
|
|
let t = start.elapsed().as_secs_f32();
|
|
|
|
|
|
let mut p1 = [0u8; 512];
|
|
|
|
|
|
let mut p2 = [0u8; 512];
|
|
|
|
|
|
for (k, &a) in addr_list.iter().enumerate() {
|
|
|
|
|
|
let phase = t * 1.2 + k as f32 * 2.1;
|
|
|
|
|
|
let v = ((phase.sin() * 0.5 + 0.5) * 255.0) as u8;
|
|
|
|
|
|
let ch = a.max(1) - 1; // 0-based
|
|
|
|
|
|
if ch < 512 { p1[ch as usize] = v; } else if ch < 1024 { p2[(ch - 512) as usize] = v; }
|
|
|
|
|
|
}
|
|
|
|
|
|
let r1 = rng.next();
|
|
|
|
|
|
let r2 = rng.next();
|
|
|
|
|
|
let mut f = tpl.clone();
|
|
|
|
|
|
f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes());
|
|
|
|
|
|
f[0x2E] = 2; f[0x2F] = 0; // 节点序号 (真机 .1 = 2)
|
|
|
|
|
|
f[0x22..0x26].copy_from_slice(&r1.to_le_bytes());
|
|
|
|
|
|
f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes());
|
|
|
|
|
|
f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, b12));
|
|
|
|
|
|
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&p2, r2, b12));
|
|
|
|
|
|
if sock.send_to(&f, &dst).is_ok() { sent += 1; }
|
|
|
|
|
|
seq += 1;
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(33));
|
|
|
|
|
|
}
|
|
|
|
|
|
println!("[5] 完成: 发送 {} 帧", sent);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn found_valid(uid: &[u8; 6]) -> bool {
|
|
|
|
|
|
uid.iter().any(|&b| b != 0) && uid.iter().any(|&b| b != 0xFF)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 构造"新鲜"宣告帧: announce_live 模板 (今日 SL 实抓) + 每发随机会话 ID@0x221
|
|
|
|
|
|
/// + 当前本地时间戳@0x239 (年u16LE 月 日 时 分 秒 毫秒u16LE 时区) — Butler 按
|
|
|
|
|
|
/// 宣告时间戳判主机新鲜度, 陈旧宣告(旧抓包模板)会被当僵尸主机无视 ident/AECH
|
|
|
|
|
|
fn build_fresh_announce(rng: &mut Rng) -> Vec<u8> {
|
|
|
|
|
|
let mut a = load_template("announce_live"); // 644B
|
|
|
|
|
|
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
|
|
|
|
|
// 本地时间分解 (UTC+8, 与实测 tz=02... 用本地时区偏移)
|
|
|
|
|
|
let secs = now + tz_offset_secs();
|
|
|
|
|
|
let days = secs / 86400;
|
|
|
|
|
|
let rem = secs % 86400;
|
|
|
|
|
|
let (year, month, day) = civil_from_days(days as i64);
|
|
|
|
|
|
let hour = (rem / 3600) as u8;
|
|
|
|
|
|
let min = ((rem % 3600) / 60) as u8;
|
|
|
|
|
|
let sec = (rem % 60) as u8;
|
|
|
|
|
|
let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_millis();
|
|
|
|
|
|
// 时间戳字段实际位于 0x235: 年u16LE@0x235 月@0x237 日@0x238 时@0x239 分@0x23A
|
|
|
|
|
|
// 秒@0x23B 毫秒u16LE@0x23C 时区@0x23E (实测 2026-09-10 22:52:50.992 tz=02)
|
|
|
|
|
|
a[0x235] = (year & 0xFF) as u8;
|
|
|
|
|
|
a[0x236] = (year >> 8) as u8;
|
|
|
|
|
|
a[0x237] = month;
|
|
|
|
|
|
a[0x238] = day;
|
|
|
|
|
|
a[0x239] = hour;
|
|
|
|
|
|
a[0x23A] = min;
|
|
|
|
|
|
a[0x23B] = sec;
|
|
|
|
|
|
let msle = (ms as u16).to_le_bytes();
|
|
|
|
|
|
a[0x23C] = msle[0];
|
|
|
|
|
|
a[0x23D] = msle[1];
|
|
|
|
|
|
a[0x23E] = 0x02; // 时区字段 (实测值)
|
|
|
|
|
|
// 4B 随机会话 ID @0x21D (每发宣告都变; 0x221 的 0c 恒定)
|
|
|
|
|
|
let id = rng.next();
|
|
|
|
|
|
a[0x21D..0x221].copy_from_slice(&id.to_le_bytes());
|
|
|
|
|
|
a
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn tz_offset_secs() -> u64 {
|
|
|
|
|
|
// 本地时区偏移 (秒): 用 UTC 与本地时间差
|
|
|
|
|
|
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
|
|
|
|
|
|
let utc_days = (now.div_euclid(86400) + 4) % 7; // 0=周四
|
|
|
|
|
|
let _ = utc_days;
|
|
|
|
|
|
// 简化: 读环境 TZ 不可靠, 直接用 Windows 时区 API? 用 chrono 不行(无依赖).
|
|
|
|
|
|
// 实测帧 tz 字节恒 02, 时间字段为本地时间 (UTC+8 中 hour=22 对应当地 22 点)
|
|
|
|
|
|
// → 这里返回 +8h; 若部署在其它时区需调整
|
|
|
|
|
|
8 * 3600
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn civil_from_days(z: i64) -> (u16, u8, u8) {
|
|
|
|
|
|
// Howard Hinnant 算法: days since 1970-01-01 → (y, m, d)
|
|
|
|
|
|
let z = z + 719468;
|
|
|
|
|
|
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
|
|
|
|
|
let doe = (z - era * 146097) as u64;
|
|
|
|
|
|
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
|
|
|
|
let y = yoe as i64 + era * 400;
|
|
|
|
|
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
|
|
|
|
let mp = (5 * doy + 2) / 153;
|
|
|
|
|
|
let d = (doy - (153 * mp + 2) / 5 + 1) as u8;
|
|
|
|
|
|
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u8;
|
|
|
|
|
|
(if m <= 2 { (y + 1) as u16 } else { y as u16 }, m, d)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// ★1947 端口"密钥报文" (allports.pcap 实测): 40B ASCII 许可令牌, SL 每 ~34s 广播一轮
|
|
|
|
|
|
/// (双目标 21 帧 @200ms) — Butler 在 1947 监听, 疑为 RDM 代理的许可开关
|
|
|
|
|
|
const KEY40: &[u8] = b"BlZRQphAGxUbi7at8KEfmdZ7TdSB03b+qoC1RIA";
|
|
|
|
|
|
|
|
|
|
|
|
fn send_key_burst(sock: &UdpSocket) {
|
|
|
|
|
|
for _ in 0..21 {
|
|
|
|
|
|
let _ = sock.send_to(KEY40, "192.168.123.255:1947");
|
|
|
|
|
|
let _ = sock.send_to(KEY40, "255.255.255.255:1947");
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(200));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-11 15:03:09 +08:00
|
|
|
|
/// ★AECH 认领质询帧 (552B) 从零构造 — 2026-09-11 全字段破解:
|
|
|
|
|
|
/// 不透明区 A(0x2C-0x87)/B(0x90-0x123) Butler 不校验 (CLAIM_ZERO 消融实证: 清零后 AERE 仍 PASS), 填 0
|
|
|
|
|
|
fn build_claim_frame(host_ip: [u8; 4], nonce: u32, b12: u32) -> Vec<u8> {
|
|
|
|
|
|
let mut f = Vec::with_capacity(552);
|
|
|
|
|
|
f.extend_from_slice(b"ENet");
|
|
|
|
|
|
f.extend_from_slice(&2u32.to_le_bytes()); // type=2
|
|
|
|
|
|
f.extend_from_slice(&[0x00, 0x00, 0x1c, 0x7c]); // 目标端口 31772
|
|
|
|
|
|
f.extend_from_slice(&[0u8; 4]);
|
|
|
|
|
|
f.extend_from_slice(&[0x00, 0x00, 0x01, 0x00]);
|
|
|
|
|
|
f.extend_from_slice(&552u32.to_le_bytes()); // 0x14 len
|
|
|
|
|
|
f.extend_from_slice(&[0x05, 0x00]); // 0x18 sub=05
|
|
|
|
|
|
f.extend_from_slice(&[host_ip[3], host_ip[2], host_ip[1], host_ip[0]]); // 0x1A 主机IP 逆序
|
|
|
|
|
|
f.extend_from_slice(&[0x30, 0x0e, 0xd5, 0x59, 0xbb, 0x36]); // 0x1E 主机MAC (本机)
|
|
|
|
|
|
f.extend_from_slice(&[0u8; 4]); // 0x24
|
|
|
|
|
|
f.extend_from_slice(b"AECH"); // 0x28
|
|
|
|
|
|
f.extend_from_slice(&[0u8; 0x88 - 0x2C]); // 0x2C-0x87 不透明区A = 0
|
|
|
|
|
|
f.extend_from_slice(&nonce.to_le_bytes()); // 0x88 nonce
|
|
|
|
|
|
f.extend_from_slice(&b12.to_le_bytes()); // 0x8C B12 (DMX rot 基值)
|
|
|
|
|
|
f.extend_from_slice(&[0u8; 552 - 0x90]); // 0x90-0x227 不透明区B+零填充
|
|
|
|
|
|
f
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-11 12:03:43 +08:00
|
|
|
|
/// 统一会话环 (quick): DMX 30fps 持续 + RDM 穿插
|
|
|
|
|
|
/// 阶段: 0-2s 黑场(设备预热) → UN_MUTE + UNIQUE_BRANCH 二分发现 → MUTE 已发现灯具
|
|
|
|
|
|
/// → GET DEVICE_INFO/DMX 地址 → 呼吸图案控制 (未发现则用已知地址)
|
|
|
|
|
|
fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u32, secs: u64) {
|
|
|
|
|
|
let tpl = load_template("dmx_a");
|
|
|
|
|
|
let hb = load_template("heartbeat");
|
|
|
|
|
|
let known: [[u8; 6]; 3] = [
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2D DMX101
|
|
|
|
|
|
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680S DMX112
|
|
|
|
|
|
[0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32
|
|
|
|
|
|
];
|
|
|
|
|
|
let mut buf = [0u8; 4096];
|
|
|
|
|
|
let mut fixtures: Vec<[u8; 6]> = Vec::new();
|
|
|
|
|
|
let mut addr_list: Vec<u16> = Vec::new();
|
|
|
|
|
|
let mut tn: u8 = rng.byte();
|
|
|
|
|
|
let mut qseq: u16 = 1;
|
|
|
|
|
|
let mut seq: u32 = 1;
|
|
|
|
|
|
// 发现任务队列: ★窄分支优先 (已知灯具各一个只含自己的分支 — Butler 可能拒转全范围查询)
|
|
|
|
|
|
let mut stack: Vec<(u64, u64)> = vec![(0, 0xFFFF_FFFF_FFFF)];
|
|
|
|
|
|
for u in known.iter() {
|
|
|
|
|
|
let v = uid_u64(u);
|
|
|
|
|
|
stack.push((v - 1, v + 1));
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut get_queue: Vec<[u8; 6]> = Vec::new();
|
|
|
|
|
|
let mut muted: Vec<[u8; 6]> = Vec::new();
|
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
|
let mut last_hb = Instant::now();
|
|
|
|
|
|
let mut last_key_t: f32 = -100.0;
|
|
|
|
|
|
let mut last_rdm = Instant::now() - Duration::from_secs(1);
|
|
|
|
|
|
let mut unmuting = false;
|
|
|
|
|
|
let mut discovering = true;
|
|
|
|
|
|
let mut collecting = false;
|
|
|
|
|
|
let mut collect_until = Instant::now();
|
|
|
|
|
|
let mut found: Vec<[u8; 6]> = Vec::new();
|
|
|
|
|
|
let mut collision = false;
|
|
|
|
|
|
let mut current_branch: Option<(u64, u64)> = None;
|
|
|
|
|
|
let mut sent = 0u64;
|
|
|
|
|
|
|
|
|
|
|
|
println!("[S] 统一会话环启动: DMX 30fps + RDM 穿插 ({}s)", secs);
|
|
|
|
|
|
while start.elapsed() < Duration::from_secs(secs) {
|
|
|
|
|
|
let t = start.elapsed().as_secs_f32();
|
|
|
|
|
|
|
|
|
|
|
|
// --- 心跳每 6s + 密钥每 30s + ★EUCE 每 6s (SL 全程 6s 节奏, 许可信标) ---
|
|
|
|
|
|
if last_hb.elapsed() >= Duration::from_secs(6) {
|
|
|
|
|
|
let _ = sock.send_to(&hb, dst);
|
|
|
|
|
|
let euce = load_template("euce");
|
|
|
|
|
|
let _ = sock.send_to(&euce, "255.255.255.255:31772");
|
|
|
|
|
|
last_hb = Instant::now();
|
|
|
|
|
|
}
|
|
|
|
|
|
if t - last_key_t >= 30.0 {
|
|
|
|
|
|
let _ = sock.send_to(KEY40, "192.168.123.255:1947");
|
|
|
|
|
|
let _ = sock.send_to(KEY40, "255.255.255.255:1947");
|
|
|
|
|
|
last_key_t = t;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- RDM 任务 (每 ~100ms 一发; 窄分支优先, t>3s 即开始快速定位问题) ---
|
|
|
|
|
|
if discovering && t > 3.0 && last_rdm.elapsed() >= Duration::from_millis(100) {
|
|
|
|
|
|
last_rdm = Instant::now();
|
|
|
|
|
|
if collecting && Instant::now() >= collect_until {
|
|
|
|
|
|
// 本分支收集结束 → 判定
|
|
|
|
|
|
collecting = false;
|
|
|
|
|
|
if let Some((lo, hi)) = current_branch.take() {
|
|
|
|
|
|
if collision {
|
|
|
|
|
|
let mid = lo + (hi - lo) / 2;
|
|
|
|
|
|
stack.push((mid + 1, hi));
|
|
|
|
|
|
stack.push((lo, mid));
|
|
|
|
|
|
println!(" [S] 分支冲突 → 二分");
|
|
|
|
|
|
} else if found.len() == 1 && found_valid(&found[0]) {
|
|
|
|
|
|
let uid = found[0];
|
|
|
|
|
|
println!(" [S] ★发现灯具 {}", uid_s(&uid));
|
|
|
|
|
|
if !fixtures.contains(&uid) { fixtures.push(uid); }
|
|
|
|
|
|
get_queue.push(uid);
|
|
|
|
|
|
// DISC_MUTE 该灯具
|
|
|
|
|
|
tn = tn.wrapping_add(1);
|
|
|
|
|
|
let f = build_rdm_frame(&uid, tn, qseq, 0x10, 0x0002, &[]);
|
|
|
|
|
|
qseq += 1;
|
|
|
|
|
|
let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst);
|
|
|
|
|
|
} else if found.len() > 1 {
|
|
|
|
|
|
let mid = lo + (hi - lo) / 2;
|
|
|
|
|
|
stack.push((mid + 1, hi));
|
|
|
|
|
|
stack.push((lo, mid));
|
|
|
|
|
|
}
|
|
|
|
|
|
found.clear();
|
|
|
|
|
|
collision = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if collecting {
|
|
|
|
|
|
// 收集窗口内每 150ms 重发同分支查询 (真主机连发节奏)
|
|
|
|
|
|
if last_rdm.elapsed() >= Duration::from_millis(150) {
|
|
|
|
|
|
last_rdm = Instant::now();
|
|
|
|
|
|
if let Some((lo, hi)) = current_branch {
|
|
|
|
|
|
let mut pd = Vec::with_capacity(12);
|
|
|
|
|
|
pd.extend_from_slice(&u64_uid(lo));
|
|
|
|
|
|
pd.extend_from_slice(&u64_uid(hi));
|
|
|
|
|
|
tn = tn.wrapping_add(1);
|
|
|
|
|
|
let mut f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0001, &pd);
|
|
|
|
|
|
qseq += 1;
|
|
|
|
|
|
f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes());
|
|
|
|
|
|
seq += 1;
|
|
|
|
|
|
let _ = sock.send_to(&f, dst);
|
|
|
|
|
|
let mut f2 = f.clone();
|
|
|
|
|
|
f2[0x30] = 0x01;
|
|
|
|
|
|
f2[0x49] = f2[0x49].wrapping_sub(1);
|
|
|
|
|
|
let _ = sock.send_to(&f2, dst);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !collecting {
|
|
|
|
|
|
if !unmuting {
|
|
|
|
|
|
// DISC_UN_MUTE 广播
|
|
|
|
|
|
tn = tn.wrapping_add(1);
|
|
|
|
|
|
let f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0003, &[]);
|
|
|
|
|
|
qseq += 1;
|
|
|
|
|
|
let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst);
|
|
|
|
|
|
unmuting = true;
|
|
|
|
|
|
println!("[S] DISC_UN_MUTE 已发");
|
|
|
|
|
|
} else if let Some((lo, hi)) = stack.pop() {
|
|
|
|
|
|
let mut pd = Vec::with_capacity(12);
|
|
|
|
|
|
pd.extend_from_slice(&u64_uid(lo));
|
|
|
|
|
|
pd.extend_from_slice(&u64_uid(hi));
|
|
|
|
|
|
tn = tn.wrapping_add(1);
|
|
|
|
|
|
let mut f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0001, &pd);
|
|
|
|
|
|
qseq += 1;
|
|
|
|
|
|
// SL 实测: RDM 请求成对发送 (flag=00 + flag=01 副本, 背靠背 26μs)
|
|
|
|
|
|
f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes());
|
|
|
|
|
|
seq += 1;
|
|
|
|
|
|
let _ = sock.send_to(&f, dst);
|
|
|
|
|
|
let mut f2 = f.clone();
|
|
|
|
|
|
f2[0x30] = 0x01;
|
|
|
|
|
|
// flag=01 副本的 srcUID 末字节 = fe (SL 实测: ff/fe 成对)
|
|
|
|
|
|
f2[0x49] = f2[0x49].wrapping_sub(1);
|
|
|
|
|
|
let _ = sock.send_to(&f2, dst);
|
|
|
|
|
|
current_branch = Some((lo, hi));
|
|
|
|
|
|
collecting = true;
|
|
|
|
|
|
collect_until = Instant::now() + Duration::from_millis(500);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if fixtures.is_empty() && t < 90.0 {
|
|
|
|
|
|
// 未发现 → 重新压栈全范围, 循环重试 (SL 实测: 认领后 ~27.5s RDM 才开始被代理!)
|
|
|
|
|
|
stack.push((0, 0xFFFF_FFFF_FFFF));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
discovering = false;
|
|
|
|
|
|
println!("[S] 发现完成: {} 台灯具", fixtures.len());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- 收包 (非阻塞): 脉冲/参数应答 ---
|
|
|
|
|
|
while let Ok((n, src)) = sock.recv_from(&mut buf) {
|
|
|
|
|
|
if src.ip().to_string() != target { continue; }
|
|
|
|
|
|
let b = &buf[..n];
|
|
|
|
|
|
if let Some((is_pulse, d)) = rdm_region(b) {
|
|
|
|
|
|
if is_pulse && collecting {
|
|
|
|
|
|
match decode_pulse(d) {
|
|
|
|
|
|
Some(uid) => { if !found.contains(&uid) { found.push(uid); } }
|
|
|
|
|
|
None => collision = true,
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if !is_pulse && d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
|
|
|
|
|
|
let addr = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]);
|
|
|
|
|
|
let footprint = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]);
|
|
|
|
|
|
println!(" [S] ★{} → DMX地址 {} / {}通道", uid_s(&[d[9], d[10], d[11], d[12], d[13], d[14]]), addr, footprint);
|
|
|
|
|
|
addr_list.push(addr);
|
|
|
|
|
|
} else if !is_pulse && d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0xF0] {
|
|
|
|
|
|
let addr = u16::from_be_bytes([d[0x18], d[0x19]]);
|
|
|
|
|
|
println!(" [S] ★DMX_START_ADDRESS 应答: {}", addr);
|
|
|
|
|
|
addr_list.push(addr);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- GET 队列 (发现后逐台读参数) ---
|
|
|
|
|
|
if !discovering && !get_queue.is_empty() && last_rdm.elapsed() >= Duration::from_millis(120) {
|
|
|
|
|
|
last_rdm = Instant::now();
|
|
|
|
|
|
let uid = get_queue.remove(0);
|
|
|
|
|
|
tn = tn.wrapping_add(1);
|
|
|
|
|
|
let f = build_rdm_frame(&uid, tn, qseq, 0x20, 0x0060, &[]);
|
|
|
|
|
|
qseq += 1;
|
|
|
|
|
|
let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst);
|
|
|
|
|
|
let _ = muted;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- DMX 帧 @30fps ---
|
|
|
|
|
|
let mut p1 = [0u8; 512];
|
|
|
|
|
|
let mut p2 = [0u8; 512];
|
|
|
|
|
|
if !addr_list.is_empty() {
|
|
|
|
|
|
for (k, &a) in addr_list.iter().enumerate() {
|
|
|
|
|
|
let phase = t * 1.2 + k as f32 * 2.1;
|
|
|
|
|
|
let v = (((phase.sin() * 0.5 + 0.5) * 255.0) * (t * 0.5).min(1.0)) as u8; // 淡入
|
|
|
|
|
|
let ch = a.max(1) - 1;
|
|
|
|
|
|
if ch < 512 { p1[ch as usize] = v; } else if ch < 1024 { p2[(ch - 512) as usize] = v; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let r1 = rng.next();
|
|
|
|
|
|
let r2 = rng.next();
|
|
|
|
|
|
let mut f = tpl.clone();
|
|
|
|
|
|
f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes());
|
|
|
|
|
|
f[0x2E] = 2; f[0x2F] = 0;
|
|
|
|
|
|
f[0x22..0x26].copy_from_slice(&r1.to_le_bytes());
|
|
|
|
|
|
f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes());
|
|
|
|
|
|
f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, b12));
|
|
|
|
|
|
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&p2, r2, b12));
|
|
|
|
|
|
let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst);
|
|
|
|
|
|
seq += 1;
|
|
|
|
|
|
sent += 1;
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(33));
|
|
|
|
|
|
}
|
|
|
|
|
|
println!("[S] 会话环结束: DMX {} 帧, 发现灯具 {} 台, 控制地址 {:?}", sent, fixtures.len(), addr_list);
|
|
|
|
|
|
}
|