use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use laser_dac::{list_devices, open_device, DacType, LaserPoint, Result, StreamConfig};
const PPS: u32 = 30_000;
const CIRCLE_POINTS: usize = 474;
const RUN_SECS: u64 = 16;
const DISARM_AT: Duration = Duration::from_secs(6);
const REARM_AT: Duration = Duration::from_secs(8);
fn blank_circle(n: usize) -> Vec<LaserPoint> {
(0..n)
.map(|i| {
let a = (i as f64 / n as f64) * std::f64::consts::TAU;
LaserPoint::blanked(0.8 * a.cos() as f32, 0.8 * a.sin() as f32)
})
.collect()
}
fn main() -> Result<()> {
env_logger::init();
println!("Scanning for DACs...");
let devices = list_devices()?;
let info = devices
.iter()
.find(|d| d.kind == DacType::LaserCubeUsb)
.ok_or_else(|| laser_dac::Error::disconnected("no LaserCube USB (LaserDock) found"))?;
println!(" Using: {} ({})", info.name, info.kind);
let device = open_device(&info.id)?;
let (stream, info) = device.start_stream(StreamConfig::new(PPS))?;
println!(
" output_model={:?} pps={PPS} chunk-produced-by-adapter\n",
info.caps.output_model
);
let control = stream.control();
let points_sent = Arc::new(AtomicU64::new(0));
let circle = blank_circle(CIRCLE_POINTS);
let counter = Arc::clone(&points_sent);
let mut cursor = 0usize;
let producer = move |req: &laser_dac::ChunkRequest, buf: &mut [LaserPoint]| {
let n = req.target_points.min(buf.len());
for slot in buf.iter_mut().take(n) {
*slot = circle[cursor % CIRCLE_POINTS];
cursor += 1;
}
counter.fetch_add(n as u64, Ordering::Relaxed);
laser_dac::ChunkResult::Filled(n)
};
let ctl = control.clone();
let counter = Arc::clone(&points_sent);
let telemetry = std::thread::spawn(move || {
ctl.arm().unwrap();
let start = Instant::now();
let mut last = 0u64;
let mut last_t = start;
let mut disarmed = false;
let mut rearmed = false;
let mut min_armed_rate = f64::INFINITY;
println!(" t(s) interval_rate(samples/s) state");
loop {
std::thread::sleep(Duration::from_millis(500));
let elapsed = start.elapsed();
if !disarmed && elapsed >= DISARM_AT {
ctl.disarm().unwrap();
disarmed = true;
println!(" --- disarm() at t={:.1}s ---", elapsed.as_secs_f64());
}
if !rearmed && elapsed >= REARM_AT {
ctl.arm().unwrap();
rearmed = true;
println!(" --- arm() at t={:.1}s ---", elapsed.as_secs_f64());
}
let now = Instant::now();
let count = counter.load(Ordering::Relaxed);
let dt = now.duration_since(last_t).as_secs_f64();
let rate = (count - last) as f64 / dt;
let armed_now = !disarmed || rearmed;
let state = if armed_now { "armed" } else { "DISARMED" };
println!(
" {:>4.1} {:>22.0} {}",
elapsed.as_secs_f64(),
rate,
state
);
if armed_now
&& elapsed > Duration::from_secs(1)
&& !(rearmed && elapsed < REARM_AT + Duration::from_millis(600))
{
min_armed_rate = min_armed_rate.min(rate);
}
last = count;
last_t = now;
if elapsed >= Duration::from_secs(RUN_SECS) {
break;
}
}
ctl.stop().unwrap();
min_armed_rate
});
let run_start = Instant::now();
let exit = stream.run(producer, |err| eprintln!(" stream error: {err}"))?;
let run_dt = run_start.elapsed().as_secs_f64();
let min_armed_rate = telemetry.join().unwrap();
let total = points_sent.load(Ordering::Relaxed);
let armed_secs = run_dt - (REARM_AT - DISARM_AT).as_secs_f64();
println!("\n=== Summary ===");
println!(" run exit: {exit:?} (thread did not wedge)");
println!(" total points delivered: {total}");
println!(
" mean throughput over armed time (~{armed_secs:.1}s): {:.0} samples/s (commanded {PPS})",
total as f64 / armed_secs
);
println!(" min per-interval armed rate: {min_armed_rate:.0} samples/s");
let ok = min_armed_rate > 27_000.0;
println!(
" VERDICT: {} (armed throughput within ~10% of commanded, no wedge)",
if ok { "PASS" } else { "FAIL" }
);
Ok(())
}