diff --git a/src/hostdemo.rs b/src/hostdemo.rs index 6533610..205172e 100644 --- a/src/hostdemo.rs +++ b/src/hostdemo.rs @@ -1691,7 +1691,6 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u ]; let mut buf = [0u8; 4096]; let mut fixtures: Vec<[u8; 6]> = Vec::new(); - let mut addr_list: Vec = Vec::new(); let mut tn: u8 = rng.byte(); let mut qseq: u16 = 1; let mut seq: u32 = 1; @@ -1701,7 +1700,33 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u let v = uid_u64(u); stack.push((v - 1, v + 1)); } - let mut get_queue: Vec<[u8; 6]> = Vec::new(); + /// 一台灯具的参数快照 (参数扫描汇总用) + struct DevInfo { + uid: [u8; 6], + addr: u16, + footprint: u16, + proto_major: u8, + proto_minor: u8, + model: u16, + sw: [u8; 4], + sensors: u16, + label: Option, + model_desc: Option, + sw_label: Option, + manufacturer: Option, + } + let mut get_queue: Vec<([u8; 6], u16)> = Vec::new(); + let mut devinfo: Vec = Vec::new(); + let mut param_sweep_started = false; + let mut param_summary_done = false; + let mut sweep_round: u32 = 0; + let mut answered: Vec<([u8; 6], u16)> = Vec::new(); + let mut last_sweep_rx = Instant::now(); + let mut addr_by_uid: Vec<([u8; 6], u16)> = Vec::new(); + let mut last_reply: Option<([u8; 6], u16, Instant)> = None; + let mut status_poll_count: u32 = 0; + let mut next_status_poll = Instant::now(); + const STATUS_POLL_INTERVAL: Duration = Duration::from_secs(6); let mut muted: Vec<[u8; 6]> = Vec::new(); let start = Instant::now(); let mut last_hb = Instant::now(); @@ -1749,7 +1774,7 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u let uid = found[0]; println!(" [S] ★发现灯具 {}", uid_s(&uid)); if !fixtures.contains(&uid) { fixtures.push(uid); } - get_queue.push(uid); + // 参数扫描队列在发现完成后按 PID 填充 (见下) // DISC_MUTE 该灯具 tn = tn.wrapping_add(1); let f = build_rdm_frame(&uid, tn, qseq, 0x10, 0x0002, &[]); @@ -1836,34 +1861,145 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u None => collision = true, } } else if !is_pulse && d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let ruid = [d[9], d[10], d[11], d[12], d[13], d[14]]; 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); + // 成对副本去重 (flag=00/01 各上行一份, 背靠背到达) + if let Some((lu, lp, lt)) = last_reply { + if lu == ruid && lp == 0x0060 && lt.elapsed() < Duration::from_millis(200) { continue; } + } + last_reply = Some((ruid, 0x0060, Instant::now())); + if !answered.contains(&(ruid, 0x0060)) { answered.push((ruid, 0x0060)); } + last_sweep_rx = Instant::now(); + if let Some(e) = devinfo.iter_mut().find(|x| x.uid == ruid) { + if e.addr != addr { println!(" [S] ★{} DMX地址变更: {} → {}", uid_s(&ruid), e.addr, addr); } + if e.footprint != footprint { println!(" [S] ★{} 通道数变更: {} → {}", uid_s(&ruid), e.footprint, footprint); } + e.addr = addr; e.footprint = footprint; + } + if let Some(ae) = addr_by_uid.iter_mut().find(|(u, _)| *u == ruid) { ae.1 = addr; } + else { addr_by_uid.push((ruid, addr)); } + if param_summary_done { + // 周期状态轮询: 每台一行紧凑状态 + println!(" [S] 轮询#{} {} @DMX{} {}ch", status_poll_count, uid_s(&ruid), addr, footprint); + } else { + // E1.20 §10.4.1 DEVICE_INFO PD: [0]协议主 [1]协议次 [2..4]型号 [4..6]产品类 + // [6..10]软件版 [10..12]footprint [12]personality [13..15]子设备数 [15..17]传感器数 + let pmaj = d[0x18]; let pmin = d[0x18 + 1]; + let model = u16::from_be_bytes([d[0x18 + 2], d[0x18 + 3]]); + let pers = d[0x18 + 12]; + let subdev = u16::from_be_bytes([d[0x18 + 13], d[0x18 + 14]]); + let sensors = u16::from_be_bytes([d[0x18 + 15], d[0x18 + 16]]); + println!(" [S] ★{} DEVICE_INFO: 协议{}.{}/模型{:04x}/固件{:02x}{:02x}{:02x}{:02x} footprint={} personality=0x{:02x} 子设备={} 传感器={}", + uid_s(&ruid), pmaj, pmin, model, d[0x18+6], d[0x18+7], d[0x18+8], d[0x18+9], + footprint, pers, subdev, sensors); + println!(" [S] PD({:02x}): {} @addr={}", d.len() - 0x18, hexs(&d[0x18..]), addr); + let sw = [d[0x18 + 6], d[0x18 + 7], d[0x18 + 8], d[0x18 + 9]]; + if let Some(e) = devinfo.iter_mut().find(|x| x.uid == ruid) { + e.proto_major = pmaj; e.proto_minor = pmin; e.model = model; e.sw = sw; e.sensors = sensors; + } else { + devinfo.push(DevInfo { uid: ruid, addr, footprint, proto_major: pmaj, proto_minor: pmin, + model, sw, sensors, label: None, model_desc: None, sw_label: None, manufacturer: None }); + } + } } else if !is_pulse && d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0xF0] { + let ruid = [d[9], d[10], d[11], d[12], d[13], d[14]]; let addr = u16::from_be_bytes([d[0x18], d[0x19]]); - println!(" [S] ★DMX_START_ADDRESS 应答: {}", addr); - addr_list.push(addr); + if let Some((lu, lp, lt)) = last_reply { + if lu == ruid && lp == 0x00F0 && lt.elapsed() < Duration::from_millis(200) { continue; } + } + last_reply = Some((ruid, 0x00F0, Instant::now())); + if !answered.contains(&(ruid, 0x00F0)) { answered.push((ruid, 0x00F0)); } + last_sweep_rx = Instant::now(); + println!(" [S] ★{} DMX_START_ADDRESS = {}", uid_s(&ruid), addr); + if let Some(e) = devinfo.iter_mut().find(|x| x.uid == ruid) { e.addr = addr; } + if let Some(ae) = addr_by_uid.iter_mut().find(|(u, _)| *u == ruid) { ae.1 = addr; } + else { addr_by_uid.push((ruid, addr)); } + } else if !is_pulse && d.len() >= 0x18 && d[0] == 0xCC + && matches!(g16(d, 0x15), 0x0081 | 0x0082 | 0x00C0 | 0x0080) { + let ruid = [d[9], d[10], d[11], d[12], d[13], d[14]]; + let pid = g16(d, 0x15); + if let Some((lu, lp, lt)) = last_reply { + if lu == ruid && lp == pid && lt.elapsed() < Duration::from_millis(200) { continue; } + } + last_reply = Some((ruid, pid, Instant::now())); + if !answered.contains(&(ruid, pid)) { answered.push((ruid, pid)); } + last_sweep_rx = Instant::now(); + let (name, field) = match pid { + 0x0081 => ("DEVICE_LABEL", 0usize), + 0x0082 => ("MODEL_DESCRIPTION", 1usize), + 0x00C0 => ("SOFTWARE_VERSION", 2usize), + _ => ("MANUFACTURER_LABEL", 3usize), + }; + if d.len() > 0x18 { + let txt: String = d[0x18..].iter().map(|&c| if (0x20..0x7f).contains(&c) { c as char } else { '.' }).collect(); + println!(" [S] ★{} {} = '{}'", uid_s(&ruid), name, txt); + if let Some(e) = devinfo.iter_mut().find(|x| x.uid == ruid) { + let owned = txt.trim_end_matches('.').to_string(); + match field { 0 => e.label = Some(owned), 1 => e.model_desc = Some(owned), + 2 => e.sw_label = Some(owned), _ => e.manufacturer = Some(owned) } + } + } else { + println!(" [S] ★{} {} = (无 PD, PDL=0x{:02x})", uid_s(&ruid), name, d[0x17]); + } } } } - // --- GET 队列 (发现后逐台读参数) --- + // --- 参数扫描 (发现完成后, 带 2 轮重试) + 周期 RDM 状态轮询 --- + const SWEEP_PIDS: [u16; 6] = [0x0060, 0x00F0, 0x0081, 0x0082, 0x00C0, 0x0080]; + if !discovering && get_queue.is_empty() && !fixtures.is_empty() && !param_sweep_started { + param_sweep_started = true; + for uid in fixtures.iter() { + for pid in SWEEP_PIDS { get_queue.push((*uid, pid)); } + } + println!("[S] 参数扫描开始: {} 台 × {} PID", fixtures.len(), SWEEP_PIDS.len()); + } if !discovering && !get_queue.is_empty() && last_rdm.elapsed() >= Duration::from_millis(120) { last_rdm = Instant::now(); - let uid = get_queue.remove(0); + let (uid, pid) = get_queue.remove(0); tn = tn.wrapping_add(1); - let f = build_rdm_frame(&uid, tn, qseq, 0x20, 0x0060, &[]); + let f = build_rdm_frame(&uid, tn, qseq, 0x20, pid, &[]); 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; + } else if param_sweep_started && get_queue.is_empty() { + // 队列排空: 要么重试未应答项, 要么出汇总, 要么进入周期状态轮询 + let unanswered: Vec<([u8; 6], u16)> = fixtures.iter() + .flat_map(|u| SWEEP_PIDS.iter().map(move |p| (*u, *p))) + .filter(|key| !answered.contains(key)) + .collect(); + if !param_summary_done { + if !unanswered.is_empty() && sweep_round < 2 { + sweep_round += 1; + for key in unanswered.iter() { get_queue.push(*key); } + println!("[S] 参数扫描重试 第{}轮 ({} 项未应答)", sweep_round, unanswered.len()); + } else if unanswered.is_empty() || last_sweep_rx.elapsed() >= Duration::from_millis(2500) { + param_summary_done = true; + next_status_poll = Instant::now() + Duration::from_secs(5); + println!("[S] ===== 参数汇总 ====="); + for f in devinfo.iter() { + println!(" [S] {} @DMX{} {}ch 协议{}.{}/{:04x} 固件{:02x}{:02x}{:02x}{:02x} 型号{} 厂商{} 名称{} SW:{}", + uid_s(&f.uid), f.addr, f.footprint, f.proto_major, f.proto_minor, f.model, + f.sw[0], f.sw[1], f.sw[2], f.sw[3], + f.model_desc.as_deref().unwrap_or("-"), + f.manufacturer.as_deref().unwrap_or("-"), + f.label.as_deref().unwrap_or("-"), + f.sw_label.as_deref().unwrap_or("-")); + } + println!("[S] ==================== (每 {}s 轮询一次 RDM 状态)", STATUS_POLL_INTERVAL.as_secs()); + } + } else if Instant::now() >= next_status_poll && !fixtures.is_empty() { + status_poll_count += 1; + for uid in fixtures.iter() { get_queue.push((*uid, 0x0060)); } + next_status_poll = Instant::now() + STATUS_POLL_INTERVAL; + } } // --- 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() { + if !addr_by_uid.is_empty() { + for (k, &(_, a)) in addr_by_uid.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; @@ -1884,7 +2020,8 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u sent += 1; std::thread::sleep(Duration::from_millis(33)); } - println!("[S] 会话环结束: DMX {} 帧, 发现灯具 {} 台, 控制地址 {:?}", sent, fixtures.len(), addr_list); + println!("[S] 会话环结束: DMX {} 帧, 发现灯具 {} 台, 控制地址 {:?}", sent, fixtures.len(), + addr_by_uid.iter().map(|(u, a)| format!("{}@{}", uid_s(u), a)).collect::>()); } /// ★UDP 31772 通用中继 (udprelay 子命令): 跨网段主机验证用