From 76d547b2020d6c780572d952de0260366fd65d52 Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Fri, 11 Apr 2025 20:09:33 +0200 Subject: [PATCH] Interface that does not hang and controls laser settings --- src/bin/render_lines_gui.rs | 488 ++++++++++++++++++++++++++++++++++++ src/trap/laser.rs | 2 +- 2 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 src/bin/render_lines_gui.rs diff --git a/src/bin/render_lines_gui.rs b/src/bin/render_lines_gui.rs new file mode 100644 index 0000000..5cba283 --- /dev/null +++ b/src/bin/render_lines_gui.rs @@ -0,0 +1,488 @@ +//! From https://github.com/seem-less/nannou/blob/helios_laser_DAC/examples/laser/laser_frame_stream_gui.rs +//! A clone of the `laser_frame_stream.rs` example that allows for configuring laser settings via a +//! UI. + +use bevy::math::Mat3; +use nannou::geom::Rect; +use nannou::prelude::*; +use nannou_egui::{self, egui, Egui}; +use nannou_laser as laser; +use serde_json::Result; +use trap_rust::trap::{laser::{apply_homography_matrix, python_cv_h_into_mat3, LaserModel, TMP_PYTHON_LASER_H}, tracks::{LaserPoints, RenderableLines}}; +use zmq::Socket; +use std::sync::{mpsc, Arc}; + +fn main() { + nannou::app(model).update(update).run(); +} + +struct Model { + // A handle to the laser API used for spawning streams and detecting DACs. + laser_api: Arc, + // All of the live stream handles. + laser_streams: Vec>, + // A copy of the state that will live on the laser thread so we can present a GUI. + laser_model: LaserModel, + // A copy of the laser settings so that we can control them with the GUI. + laser_settings: LaserSettings, + // For receiving newly detected DACs. + dac_rx: mpsc::Receiver, + // The UI for control over laser parameters and settings. + egui: Egui, + // socket for receiving points + zmq: Socket, + current_points: LaserPoints, // a copy for the drawing renderer +} + +struct LaserSettings { + point_hz: u32, + latency_points: u32, + frame_hz: u32, + enable_optimisations: bool, + distance_per_point: f32, + blank_delay_points: u32, + radians_per_point: f32, +} + +#[derive(Clone, Copy)] +struct RgbProfile { + rgb: [f32; 3], +} + + +impl Default for LaserSettings { + fn default() -> Self { + use laser::stream; + use laser::stream::frame::InterpolationConfig; + LaserSettings { + point_hz: stream::DEFAULT_POINT_HZ, + latency_points: stream::points_per_frame( + stream::DEFAULT_POINT_HZ, + stream::DEFAULT_FRAME_HZ, + ), + frame_hz: stream::DEFAULT_FRAME_HZ, + enable_optimisations: true, + distance_per_point: InterpolationConfig::DEFAULT_DISTANCE_PER_POINT, + blank_delay_points: InterpolationConfig::DEFAULT_BLANK_DELAY_POINTS, + radians_per_point: InterpolationConfig::DEFAULT_RADIANS_PER_POINT, + } + } +} + +impl Default for RgbProfile { + fn default() -> Self { + RgbProfile { rgb: [1.0; 3] } + } +} + +fn setup_zmq() -> Socket{ + let url = "tcp://100.109.175.82:99174"; + let context = zmq::Context::new(); + let subscriber = context.socket(zmq::SUB).unwrap(); + assert!(subscriber.connect(url).is_ok()); + + // let filter = "10001"; + let filter = ""; //"msgs"; + assert!(subscriber.set_subscribe(filter.as_bytes()).is_ok()); + + subscriber +} + +// fn zmq_receive(subscriber: &Socket, laser_streams: &Vec>) { +fn zmq_receive(model: &mut Model) { + let subscriber = &model.zmq; + let mut items = [ + subscriber.as_poll_item(zmq::POLLIN) + ]; + let _nr = zmq::poll(&mut items, 0).unwrap(); + + if items[0].is_readable() { + let json = subscriber.recv_string(0).unwrap().unwrap(); + // dbg!(&json[4..]); + + // let msg: Frame = serde_json::from_str(&json[4..]).expect("No valid json?"); + let res: Result = serde_json::from_str(&json); + + let lines: RenderableLines = match res { + Ok(lines) => lines, // if Ok(255), set x to 255 + Err(_e) => { + println!("No valid json?"); + println!("{}", _e); + return + }, // if Err("some message"), panic with error message "some message" + }; + + // println!("receive {}", lines.lines.len()); + + for laser_stream in (&model.laser_streams).into_iter() { + // let lines = get_laser_lines(version); + let points: LaserPoints = (&lines).into(); + laser_stream.send(|laser| { + let laser_points: LaserPoints = points; + laser.current_points = laser_points; + }).unwrap(); + } + + model.current_points = (&lines).into(); + + } +} + +fn model(app: &App) -> Model { + // Create a window to receive keyboard events. + let w_id = app + .new_window() + .size(312, 530) + // .key_pressed(key_pressed) + .raw_event(raw_window_event) + .view(view) + .build() + .unwrap(); + + // Initialise the state that we want to live on the laser thread and spawn the stream. + let laser_settings = LaserSettings::default(); + let laser_model = LaserModel::new(); + let zmq = setup_zmq(); + + // TODO Implement `Clone` for `Api` so that we don't have to `Arc` it. + let laser_api = Arc::new(laser::Api::new()); + + // A channel for receiving newly detected DACs. + let (dac_tx, dac_rx) = mpsc::channel(); + + // Spawn a thread for detecting the DACs. + let laser_api2 = laser_api.clone(); + std::thread::spawn(move || { + let mut detected = std::collections::HashSet::new(); + + // detect Helios DACs first since they can't be detected while simultaneously sending data to them + for res in laser_api2.detect_dacs(laser::DacVariant::DacVariantHelios) { + if let laser::DetectDacs::Helios { previous_dac } = res { + if !detected.insert(laser::DetectedDac::from(previous_dac).id()) { + break; + } + } + } + for detected_helios in &detected { + if let laser::dac_manager::Id::Helios { id } = *detected_helios { + let dac: laser::helios_dac::NativeHeliosDacParams = id.into(); + println!("{:#?}", dac); + if dac_tx.send(dac.into()).is_err() { + break; + } + } + } + + // for Etherdream DAC + for res in laser_api2 + .detect_dacs(laser::DacVariant::DacVariantEtherdream) + .expect("failed to start detecting Etherdream DACs") + { + let dac = res.expect("error occurred during DAC detection"); + if detected.insert(dac.id()) { + println!("{:#?}", dac); + if dac_tx.send(dac).is_err() { + break; + } + } + } + }); + + // We'll use a `Vec` to collect laser streams as they appear. + let laser_streams = vec![]; + + // A user-interface to tweak the settings. + let window = app.window(w_id).unwrap(); + let egui = Egui::from_window(&window); + // egui.ctx().set_fonts(fonts()); + egui.ctx().set_style(style()); + + let current_points = Vec::new(); + + Model { + laser_api, + laser_settings, + laser_model, + laser_streams, + dac_rx, + egui, + zmq, + current_points, + } +} + +// Draw lines or points based on the `DrawMode`. +// fn add_points(points: I, scale: f32, frame: &mut laser::Frame) +// where +// I: IntoIterator, +// I::Item: AsRef, +// { +// let points = points.into_iter().map(|p| { +// let mut p = p.as_ref().clone(); +// p.position[0] *= scale; +// p.position[1] *= scale; +// p +// }); +// frame.add_lines(points); +// } + +const LASER_H: Mat3 = python_cv_h_into_mat3(TMP_PYTHON_LASER_H); + +fn laser_frame_producer(model: &mut LaserModel, frame: &mut laser::Frame){ + + let points = model.current_points.clone(); + + let mut new_points = Vec::new(); + for point in points.into_iter() { + let p = point.position; + let new_position = apply_homography_matrix(LASER_H, &p); + // let s = 1.; // when using TMP_PYTHON_LASER_H_FOR_NANNOU -- doesn't work? + let s = 0xFFF as f32; // when using TMP_PYTHON_LASER_H + + let new_point = laser::Point { + position: [new_position[0]/s, new_position[1]/s], + .. point + }; + new_points.push(new_point); + } + + // println!("Points {}", new_points.len()); + // println!("{:?}", new_points); + frame.add_lines(new_points); +} + +fn raw_window_event(_app: &App, model: &mut Model, event: &nannou::winit::event::WindowEvent) { + model.egui.handle_raw_event(event); +} + +fn update(_app: &App, model: &mut Model, update: Update) { + // First, check for new laser DACs. + for dac in model.dac_rx.try_recv() { + println!("Detected DAC {:?}!", dac.id()); + let stream = model + .laser_api + .new_frame_stream(model.laser_model.clone(), laser_frame_producer) + .detected_dac(dac) + .build() + .expect("failed to establish stream with newly detected DAC"); + model.laser_streams.push(stream); + } + + // Check if any streams have dropped out (e.g network issues, DAC turned off) and attempt to + // start them again. + let mut dropped = vec![]; + for (i, stream) in model.laser_streams.iter().enumerate() { + if stream.is_closed() { + dropped.push(i); + } + } + for i in dropped.into_iter().rev() { + let stream = model.laser_streams.remove(i); + let dac = stream + .dac() + .expect("`dac` returned `None` even though one was specified during stream creation"); + let res = stream + .close() + .expect("stream was unexpectedly already closed from another stream handle") + .expect("failed to join stream thread"); + if let Err(err) = res { + eprintln!("Stream closed due to an error: {}", err); + } + println!("attempting to restart stream with DAC {:?}", dac.id()); + match model + .laser_api + .new_frame_stream(model.laser_model.clone(), laser_frame_producer) + .detected_dac(dac) + .build() + { + Err(err) => eprintln!("failed to restart stream: {}", err), + Ok(stream) => model.laser_streams.push(stream), + } + } + + zmq_receive(model); + + // Update the GUI. + let Model { + ref mut egui, + ref laser_streams, + ref mut laser_model, + ref mut laser_settings, + .. + } = *model; + + + egui.set_elapsed_time(update.since_start); + let ctx = egui.begin_frame(); + + // The timeline area. + egui::containers::CentralPanel::default().show(&ctx, |ui| { + fn grid_min_col_width(ui: &egui::Ui, n_options: usize) -> f32 { + let gap_space = ui.spacing().item_spacing.x * (n_options as f32 - 1.0); + let grid_w = ui.available_width(); + (grid_w - gap_space) / n_options as f32 + } + + ui.heading("Laser Points"); + + ui.separator(); + + ui.add(egui::Label::new(format!("Laser {}", model.current_points.len()))); + + ui.heading("Laser Settings"); + + if ui + .add(egui::Slider::new(&mut laser_settings.point_hz, 1_000..=10_000).text("DAC PPS")) + .changed() + { + let hz = laser_settings.point_hz; + for stream in laser_streams { + stream.set_point_hz(hz).ok(); + } + } + if ui + .add(egui::Slider::new(&mut laser_settings.latency_points, 10..=1_500).text("Latency")) + .changed() + { + let latency = laser_settings.latency_points; + for stream in laser_streams { + stream.set_latency_points(latency).ok(); + } + } + if ui + .add(egui::Slider::new(&mut laser_settings.frame_hz, 1..=120).text("Target FPS")) + .changed() + { + let hz = laser_settings.frame_hz; + for stream in laser_streams { + stream.set_frame_hz(hz).ok(); + } + } + + ui.separator(); + + ui.heading("Laser Path Interpolation"); + + if ui + .checkbox(&mut laser_settings.enable_optimisations, "Optimize Path") + .changed() + { + for stream in laser_streams { + stream + .enable_optimisations(laser_settings.enable_optimisations) + .ok(); + } + } + if ui + .add( + egui::Slider::new(&mut laser_settings.distance_per_point, 0.01..=1.0) + .text("Distance Per Point"), + ) + .changed() + { + let distance = laser_settings.distance_per_point; + for stream in laser_streams { + stream.set_distance_per_point(distance).ok(); + } + } + if ui + .add( + egui::Slider::new(&mut laser_settings.blank_delay_points, 0..=32) + .text("Blank Delay (Points)"), + ) + .changed() + { + let delay = laser_settings.blank_delay_points; + for stream in laser_streams { + stream.set_blank_delay_points(delay).ok(); + } + } + let mut degrees = rad_to_deg(laser_settings.radians_per_point); + if ui + .add(egui::Slider::new(&mut degrees, 1.0..=180.0).text("Degrees Per Point")) + .changed() + { + let radians = deg_to_rad(degrees); + laser_settings.radians_per_point = radians; + for stream in laser_streams { + stream.set_radians_per_point(radians).ok(); + } + } + + + }); +} + +// fn key_pressed(_app: &App, model: &mut Model, key: Key) { +// // Send a new pattern to the laser on keys 1, 2, 3 and 4. +// let new_pattern = match key { +// Key::Key1 => TestPattern::Rectangle, +// Key::Key2 => TestPattern::Triangle, +// Key::Key3 => TestPattern::Crosshair, +// Key::Key4 => TestPattern::ThreeVerticalLines, +// Key::Key5 => TestPattern::Circle, +// Key::Key6 => TestPattern::Spiral, +// _ => return, +// }; +// for stream in &model.laser_streams { +// stream +// .send(move |laser| laser_frame_producer.test_pattern = new_pattern) +// .ok(); +// } +// } + +fn view(_app: &App, model: &Model, frame: Frame) { + model.egui.draw_to_frame(&frame).unwrap(); +} + +// The following functions are some custom styling preferences in an attempt to improve on the +// default egui theming. + +// fn fonts() -> egui::FontDefinitions { +// let mut fonts = egui::FontDefinitions::default(); +// let entries = [ +// ( +// egui::TextStyle::Small, +// (egui::FontFamily::Proportional, 13.0), +// ), +// ( +// egui::TextStyle::Body, +// (egui::FontFamily::Proportional, 16.0), +// ), +// ( +// egui::TextStyle::Button, +// (egui::FontFamily::Proportional, 16.0), +// ), +// ( +// egui::TextStyle::Heading, +// (egui::FontFamily::Proportional, 20.0), +// ), +// ( +// egui::TextStyle::Monospace, +// (egui::FontFamily::Monospace, 14.0), +// ), +// ]; +// fonts.families.extend(entries.iter().cloned()); +// fonts +// } + +fn style() -> egui::Style { + let mut style = egui::Style::default(); + style.spacing = egui::style::Spacing { + item_spacing: egui::Vec2::splat(8.0), + // window_margin: egui::Vec2::new(6.0, 6.0), + button_padding: egui::Vec2::new(4.0, 2.0), + interact_size: egui::Vec2::new(56.0, 24.0), + indent: 10.0, + icon_width: 20.0, + icon_spacing: 1.0, + ..style.spacing + }; + style.visuals.widgets.inactive.fg_stroke.color = egui::Color32::WHITE; + style.visuals.extreme_bg_color = egui::Color32::from_gray(12); + style.visuals.faint_bg_color = egui::Color32::from_gray(24); + style.visuals.widgets.noninteractive.bg_fill = egui::Color32::from_gray(36); + style.visuals.widgets.noninteractive.bg_stroke.color = egui::Color32::BLACK; + style.visuals.widgets.noninteractive.fg_stroke.color = egui::Color32::WHITE; + style +} diff --git a/src/trap/laser.rs b/src/trap/laser.rs index 38e47b4..4b8bb21 100644 --- a/src/trap/laser.rs +++ b/src/trap/laser.rs @@ -29,7 +29,7 @@ pub fn apply_homography_matrix(h: Mat3, p: &[f32; 2]) -> [f32; 2]{ } -#[derive(Resource)] +#[derive(Resource, Clone)] pub struct LaserModel{ pub t: Instant, // register start time, so that animations can be moving pub current_points: LaserPoints