diff --git a/firmware/src/main.rs b/firmware/src/main.rs index cf2265c..9434adc 100644 --- a/firmware/src/main.rs +++ b/firmware/src/main.rs @@ -323,6 +323,7 @@ mod app { let diff = freq as i64 - last_freq as i64; last_freq = freq; + if diff.abs() > 50_000 { defmt::info!("Out of range, dropping sample."); continue; @@ -433,7 +434,7 @@ mod app { to_vec_cobs::(&device_msg).unwrap(); serial.write(bytes.as_slice()).unwrap(); } - HostMessage::SetPLLOutputs => { + HostMessage::SetPLLOutputs(_) => { defmt::error!("PLL output is not implemented yet") } }, diff --git a/hostsoftware/Cargo.toml b/hostsoftware/Cargo.toml index de921a7..ba9ef11 100644 --- a/hostsoftware/Cargo.toml +++ b/hostsoftware/Cargo.toml @@ -10,5 +10,6 @@ cheapsdo-protocol = { path = "../protocol" } crossbeam-channel = "0.5.11" eframe = "0.24.1" egui_plot = "0.24.1" +numfmt = "1.1.1" postcard = {version = "1.0.8", features = ["use-std"]} serialport = "4.3.0" diff --git a/hostsoftware/src/formatters.rs b/hostsoftware/src/formatters.rs index bdf4f3a..19e999b 100644 --- a/hostsoftware/src/formatters.rs +++ b/hostsoftware/src/formatters.rs @@ -1,12 +1,21 @@ -pub fn frequency(freq: f64) -> String { - let rest = (freq * 1_000_000_000.0) as u64; - let milli_hz = rest % 1000; - let rest = rest / 1000; - let hz = rest % 1000; - let rest = rest / 1000; - let khz = rest % 1000; - let rest = rest / 1000; - let mhz = rest % 1000; +use numfmt::{Formatter, Precision}; - format!("{:02}.{:03} {:03} {:03} MHz", mhz, khz, hz, milli_hz) +pub fn format_frequency(freq: f64) -> String { + let mut formatter = Formatter::new() + .separator(' ') + .unwrap() + .precision(Precision::Decimals(0)); + + formatter.fmt2(freq).to_owned() +} + +pub fn format_frequency_mhz(freq: f64) -> String { + let mut formatter = Formatter::new() + .separator(' ') + .unwrap() + .suffix(" Hz") + .unwrap() + .precision(Precision::Decimals(3)); + + formatter.fmt2(freq).to_owned() } diff --git a/hostsoftware/src/main.rs b/hostsoftware/src/main.rs index d3ec89d..6aad15a 100644 --- a/hostsoftware/src/main.rs +++ b/hostsoftware/src/main.rs @@ -1,5 +1,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +use eframe::egui::{self}; + use std::io; use std::io::Write; @@ -7,15 +9,12 @@ use std::time::Duration; use crossbeam_channel::{select, unbounded, Receiver, Sender}; -use eframe::egui::{self, RichText}; - - - use cheapsdo_protocol::*; use postcard::{from_bytes_cobs, to_stdvec_cobs}; mod formatters; mod plots; +mod widgets; fn main() -> Result<(), eframe::Error> { let options = eframe::NativeOptions { @@ -49,6 +48,13 @@ struct CheapsdoControl { device_state: StatusMessage, average_points: Vec<[f64; 2]>, pwm_points: Vec<[f64; 2]>, + + ms1_frequency_id: egui::Id, + ms2_frequency_id: egui::Id, + + output_frequency_id: [egui::Id; 3], + + pll_settings: PLLSettings, } impl Default for CheapsdoControl { @@ -56,12 +62,23 @@ impl Default for CheapsdoControl { Self { serial_device: "".to_owned(), serial_connected: false, + data_rx: None, cmd_tx: None, device_state: StatusMessage::default(), average_points: Vec::new(), pwm_points: Vec::new(), + + ms1_frequency_id: egui::Id::new("ms1_frequency_id"), + ms2_frequency_id: egui::Id::new("ms2_frequency_id"), + output_frequency_id: [ + egui::Id::new("output_frequency1_id"), + egui::Id::new("output_frequency2_id"), + egui::Id::new("output_frequency3_id"), + ], + + pll_settings: PLLSettings::default(), } } } @@ -80,7 +97,7 @@ impl eframe::App for CheapsdoControl { self.device_state = status_msg.clone(); self.average_points.push([ self.average_points.len() as f64, - status_msg.average_frequency as f64 / 1_000_000_000.0 + status_msg.average_frequency as f64 / 1000.0 ]); self.pwm_points.push([ self.pwm_points.len() as f64, @@ -161,41 +178,60 @@ impl eframe::App for CheapsdoControl { ui.vertical(|ui| { ui.label(egui::RichText::new("Output Settings").size(20.0)); ui.add_space(20.0); - - let mut my_string = String::new(); - - #[derive(PartialEq)] - enum Enum { - First, - Second, - } - let mut my_enum = Enum::First; - egui::Grid::new("output_pll_settings") .spacing([20.0, 5.0]) .show(ui, |ui| { - ui.label("CLK1 Multisynth"); - ui.horizontal(|ui| { - ui.selectable_value(&mut my_enum, Enum::First, "MS1"); - ui.selectable_value(&mut my_enum, Enum::Second, "MS2"); - }); + ui.label("MS1 Frequency [Hz]"); + widgets::frequency_input( + self.ms1_frequency_id, + &mut self.pll_settings.ms1_frequency, + &ctx, + ui, + ); ui.end_row(); - ui.label("CLK1 Frequency [Hz]"); - ui.add_sized([200.0, 20.0], egui::TextEdit::singleline(&mut my_string)); - ui.end_row(); - - ui.label("CLK1 Enable"); - ui.horizontal(|ui| { - ui.add(egui::Button::new( - RichText::new("On").color(egui::Color32::GREEN), - )); - ui.add(egui::Button::new( - RichText::new("Off").color(egui::Color32::RED), - )); - }); + ui.label("MS2 Frequency [Hz]"); + widgets::frequency_input( + self.ms2_frequency_id, + &mut self.pll_settings.ms2_frequency, + &ctx, + ui, + ); ui.end_row(); }); + + for i in 0..3 { + ui.separator(); + + egui::Grid::new(format!("output_clk{}_settings", i + 1)) + .spacing([20.0, 5.0]) + .show(ui, |ui| { + ui.label(format!("CLK{} Multisynth", i + 1)); + widgets::multisynth_selector( + &mut self.pll_settings.outputs[i].source, + ui, + ); + ui.end_row(); + + ui.label(format!("CLK{} Frequency [Hz]", i + 1)); + widgets::frequency_input( + self.output_frequency_id[i], + &mut self.pll_settings.outputs[i].frequency, + &ctx, + ui, + ); + ui.end_row(); + + ui.label(format!("CLK{} Enable", i + 1)); + + widgets::on_off_toggle( + &mut self.pll_settings.outputs[i].enable, + ui, + ); + + ui.end_row(); + }); + } }); }); }); diff --git a/hostsoftware/src/plots.rs b/hostsoftware/src/plots.rs index f098f64..ec92458 100644 --- a/hostsoftware/src/plots.rs +++ b/hostsoftware/src/plots.rs @@ -1,7 +1,7 @@ use eframe::egui; use egui_plot::{Line, Plot, PlotPoints}; -use crate::{formatters}; +use crate::formatters; use crate::StatusMessage; @@ -10,7 +10,7 @@ pub fn show_freuqencies(ui: &mut egui::Ui, state: &StatusMessage) { ui.label( egui::RichText::new(format!( "Measured: {}", - formatters::frequency(state.measured_frequency as f64/ 1_000_000_000.0) + formatters::format_frequency_mhz(state.measured_frequency as f64 / 1000.0) )) .family(egui::FontFamily::Monospace) .size(20.0), @@ -21,7 +21,7 @@ pub fn show_freuqencies(ui: &mut egui::Ui, state: &StatusMessage) { ui.label( egui::RichText::new(format!( "Average: {}", - formatters::frequency(state.average_frequency as f64 / 1_000_000_000.0) + formatters::format_frequency_mhz(state.average_frequency as f64 / 1000.0) )) .family(egui::FontFamily::Monospace) .size(20.0), @@ -29,7 +29,7 @@ pub fn show_freuqencies(ui: &mut egui::Ui, state: &StatusMessage) { }); } -pub fn show_plots(ui: &mut egui::Ui, average_points: &Vec<[f64; 2]>, pwm_points: &Vec<[f64; 2]>) { +pub fn show_plots(ui: &mut egui::Ui, average_points: &Vec<[f64; 2]>, pwm_points: &Vec<[f64; 2]>) { let average_line = Line::new(PlotPoints::new(average_points.clone())); Plot::new("frequency_plot") .width(900.0) @@ -39,8 +39,19 @@ pub fn show_plots(ui: &mut egui::Ui, average_points: &Vec<[f64; 2]>, pwm_points .allow_drag(false) .allow_boxed_zoom(false) .y_axis_width(12) - .y_axis_formatter(|val, _, _| formatters::frequency(val)) - .label_formatter(|name, value| format!("{}: {}", name, formatters::frequency(value.y))) + .y_axis_formatter(|val, _, _| formatters::format_frequency_mhz(val)) + .label_formatter(|name, value| { + if !name.is_empty() { + format!("{}: {}", name, formatters::format_frequency_mhz(value.y)) + } else { + format!( + "x = {}\ny= {}", + value.x, + formatters::format_frequency_mhz(value.y) + ) + } + }) + .link_cursor("plots", true, false) .show(ui, |plot_ui| { plot_ui.set_auto_bounds([true, true].into()); plot_ui.line(average_line); @@ -58,6 +69,7 @@ pub fn show_plots(ui: &mut egui::Ui, average_points: &Vec<[f64; 2]>, pwm_points .allow_drag(false) .allow_boxed_zoom(false) .y_axis_width(12) + .link_cursor("plots", true, false) .show(ui, |plot_ui| { plot_ui.set_auto_bounds([true, true].into()); plot_ui.line(pwm_line); diff --git a/hostsoftware/src/widgets.rs b/hostsoftware/src/widgets.rs new file mode 100644 index 0000000..99352d5 --- /dev/null +++ b/hostsoftware/src/widgets.rs @@ -0,0 +1,70 @@ +use cheapsdo_protocol::Multisynth; +use eframe::egui::{self, RichText}; + +use crate::formatters; + +pub fn frequency_input( + id: egui::Id, + freq: &mut u32, + ctx: &egui::Context, + ui: &mut egui::Ui, +) -> egui::Response { + let has_focus = ctx.memory(|m| m.has_focus(id)); + + let mut tmp = if has_focus { + freq.to_string() + } else { + formatters::format_frequency(*freq as f64) + }; + let resp = ui.add_sized( + [100.0, 20.0], + egui::TextEdit::singleline(&mut tmp) + .id(id) + .horizontal_align(egui::Align::Max), + ); + if has_focus { + if let Ok(val) = tmp.parse::() { + *freq = val; + } + } + + resp +} + +pub fn multisynth_selector( + selected_source: &mut Multisynth, + ui: &mut egui::Ui, +) -> egui::InnerResponse<()> { + ui.horizontal(|ui| { + ui.selectable_value(selected_source, Multisynth::MS1, "MS1"); + ui.selectable_value(selected_source, Multisynth::MS2, "MS2"); + }) +} + +pub fn on_off_toggle(toggle: &mut bool, ui: &mut egui::Ui) -> egui::InnerResponse<()> { + let on_text = if *toggle { + RichText::new("On").color(egui::Color32::GREEN) + } else { + RichText::new("On") + }; + let off_text = if *toggle { + RichText::new("Off") + } else { + RichText::new("Off").color(egui::Color32::RED) + }; + + ui.horizontal(|ui| { + if ui + .add_enabled(!*toggle, egui::Button::new(on_text)) + .clicked() + { + *toggle = true; + } + if ui + .add_enabled(*toggle, egui::Button::new(off_text)) + .clicked() + { + *toggle = false; + } + }) +} diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index e51704f..b0e4a65 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -10,7 +10,7 @@ pub enum DeviceMessage { #[derive(Serialize, Deserialize, Debug, PartialEq)] pub enum HostMessage { RequestStatus, - SetPLLOutputs, + SetPLLOutputs(PLLSettings), } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] @@ -29,3 +29,47 @@ impl Default for StatusMessage { } } } + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct PLLSettings { + pub ms1_frequency: u32, + pub ms2_frequency: u32, + pub outputs: [OutputSettings; 3], +} + +impl Default for PLLSettings { + fn default() -> Self { + Self { + ms1_frequency: 800_000_000, + ms2_frequency: 800_000_000, + outputs: [ + OutputSettings::default(), + OutputSettings::default(), + OutputSettings::default(), + ], + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct OutputSettings { + pub frequency: u32, + pub enable: bool, + pub source: Multisynth, +} + +impl Default for OutputSettings { + fn default() -> Self { + Self { + frequency: 10_000_000, + enable: false, + source: Multisynth::MS1, + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub enum Multisynth { + MS1, + MS2, +}