hostdemo: full parameter sweep + periodic RDM status polling in session_loop

After zero-pre-baked discovery, session_loop now sweeps each fixture
(DEVICE_INFO with E1.20 PD decode + raw hex, DMX_START_ADDRESS,
DEVICE_LABEL / MODEL_DESCRIPTION / SOFTWARE_VERSION / MANUFACTURER_LABEL)
with a 2-round retry for the flaky RDM window arm, prints a per-fixture
parameter summary, then enters a 6s status-poll cycle: one compact line
per fixture per cycle, address/footprint changes reported as alarms, and
playback follows a changed DMX address automatically.  Duplicate uplinks
from the flag=00/01 pair copies are deduped; the playback address list
is kept per-UID (no more duplicate control-address entries).
This commit is contained in:
tonycao 2026-09-14 21:04:45 +08:00
parent ac6ea29db9
commit 0a183e449c

View File

@ -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 buf = [0u8; 4096];
let mut fixtures: Vec<[u8; 6]> = Vec::new(); let mut fixtures: Vec<[u8; 6]> = Vec::new();
let mut addr_list: Vec<u16> = Vec::new();
let mut tn: u8 = rng.byte(); let mut tn: u8 = rng.byte();
let mut qseq: u16 = 1; let mut qseq: u16 = 1;
let mut seq: u32 = 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); let v = uid_u64(u);
stack.push((v - 1, v + 1)); 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<String>,
model_desc: Option<String>,
sw_label: Option<String>,
manufacturer: Option<String>,
}
let mut get_queue: Vec<([u8; 6], u16)> = Vec::new();
let mut devinfo: Vec<DevInfo> = 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 mut muted: Vec<[u8; 6]> = Vec::new();
let start = Instant::now(); let start = Instant::now();
let mut last_hb = 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]; let uid = found[0];
println!(" [S] ★发现灯具 {}", uid_s(&uid)); println!(" [S] ★发现灯具 {}", uid_s(&uid));
if !fixtures.contains(&uid) { fixtures.push(uid); } if !fixtures.contains(&uid) { fixtures.push(uid); }
get_queue.push(uid); // 参数扫描队列在发现完成后按 PID 填充 (见下)
// DISC_MUTE 该灯具 // DISC_MUTE 该灯具
tn = tn.wrapping_add(1); tn = tn.wrapping_add(1);
let f = build_rdm_frame(&uid, tn, qseq, 0x10, 0x0002, &[]); 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, None => collision = true,
} }
} else if !is_pulse && d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { } 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 addr = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]);
let footprint = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]); 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); // 成对副本去重 (flag=00/01 各上行一份, 背靠背到达)
addr_list.push(addr); 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] { } 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]]); let addr = u16::from_be_bytes([d[0x18], d[0x19]]);
println!(" [S] ★DMX_START_ADDRESS 应答: {}", addr); if let Some((lu, lp, lt)) = last_reply {
addr_list.push(addr); 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) { if !discovering && !get_queue.is_empty() && last_rdm.elapsed() >= Duration::from_millis(120) {
last_rdm = Instant::now(); last_rdm = Instant::now();
let uid = get_queue.remove(0); let (uid, pid) = get_queue.remove(0);
tn = tn.wrapping_add(1); 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; 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 _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst);
let _ = muted; 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 --- // --- DMX 帧 @30fps ---
let mut p1 = [0u8; 512]; let mut p1 = [0u8; 512];
let mut p2 = [0u8; 512]; let mut p2 = [0u8; 512];
if !addr_list.is_empty() { if !addr_by_uid.is_empty() {
for (k, &a) in addr_list.iter().enumerate() { for (k, &(_, a)) in addr_by_uid.iter().enumerate() {
let phase = t * 1.2 + k as f32 * 2.1; 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 v = (((phase.sin() * 0.5 + 0.5) * 255.0) * (t * 0.5).min(1.0)) as u8; // 淡入
let ch = a.max(1) - 1; 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; sent += 1;
std::thread::sleep(Duration::from_millis(33)); 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::<Vec<_>>());
} }
/// ★UDP 31772 通用中继 (udprelay 子命令): 跨网段主机验证用 /// ★UDP 31772 通用中继 (udprelay 子命令): 跨网段主机验证用