Interface that does not hang and controls laser settings
This commit is contained in:
parent
0bd763ba5e
commit
76d547b202
2 changed files with 489 additions and 1 deletions
488
src/bin/render_lines_gui.rs
Normal file
488
src/bin/render_lines_gui.rs
Normal file
|
@ -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<laser::Api>,
|
||||||
|
// All of the live stream handles.
|
||||||
|
laser_streams: Vec<laser::FrameStream<LaserModel>>,
|
||||||
|
// 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<laser::DetectedDac>,
|
||||||
|
// 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<laser::FrameStream<LaserModel>>) {
|
||||||
|
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<RenderableLines> = 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<I>(points: I, scale: f32, frame: &mut laser::Frame)
|
||||||
|
// where
|
||||||
|
// I: IntoIterator,
|
||||||
|
// I::Item: AsRef<laser::Point>,
|
||||||
|
// {
|
||||||
|
// 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
|
||||||
|
}
|
|
@ -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 struct LaserModel{
|
||||||
pub t: Instant, // register start time, so that animations can be moving
|
pub t: Instant, // register start time, so that animations can be moving
|
||||||
pub current_points: LaserPoints
|
pub current_points: LaserPoints
|
||||||
|
|
Loading…
Reference in a new issue