use futures::SinkExt; use iced::widget::{button, canvas, column, container, pick_list, row, scrollable, text, text_input}; use iced::{Element, Length, Subscription, Task, Theme}; use std::time::Duration; use tokio::sync::mpsc; use crate::ble::BleEvent; use crate::protocol::{self, EisMessage, EisPoint, Rcal, Rtia}; #[derive(Debug, Clone)] pub enum Message { BleReady(mpsc::UnboundedSender>), BleStatus(String), BleData(EisMessage), FreqStartChanged(String), FreqStopChanged(String), PpdChanged(String), RtiaSelected(Rtia), RcalSelected(Rcal), ApplySettings, StartSweep, } pub struct App { status: String, points: Vec, sweep_total: u16, freq_start: String, freq_stop: String, ppd: String, rtia: Rtia, rcal: Rcal, cmd_tx: Option>>, } impl App { pub fn new() -> (Self, Task) { (Self { status: "Starting...".into(), points: Vec::new(), sweep_total: 0, freq_start: "1000".into(), freq_stop: "200000".into(), ppd: "10".into(), rtia: Rtia::R5K, rcal: Rcal::R3K, cmd_tx: None, }, Task::none()) } pub fn title(&self) -> String { "Cue".into() } pub fn theme(&self) -> Theme { Theme::Dark } fn send_cmd(&self, sysex: &[u8]) { if let Some(tx) = &self.cmd_tx { let _ = tx.send(sysex.to_vec()); } } pub fn update(&mut self, message: Message) -> Task { match message { Message::BleReady(tx) => { self.cmd_tx = Some(tx); self.send_cmd(&protocol::build_sysex_get_config()); } Message::BleStatus(s) => self.status = s, Message::BleData(msg) => match msg { EisMessage::SweepStart { num_points, freq_start, freq_stop } => { self.points.clear(); self.sweep_total = num_points; self.status = format!( "Sweep: {} pts, {:.0}--{:.0} Hz", num_points, freq_start, freq_stop ); } EisMessage::DataPoint { point, .. } => { self.points.push(point); self.status = format!( "Receiving: {}/{}", self.points.len(), self.sweep_total ); } EisMessage::SweepEnd => { self.status = format!( "Sweep complete: {} points", self.points.len() ); } EisMessage::Config(cfg) => { self.freq_start = format!("{:.0}", cfg.freq_start); self.freq_stop = format!("{:.0}", cfg.freq_stop); self.ppd = format!("{}", cfg.ppd); self.rtia = cfg.rtia; self.rcal = cfg.rcal; self.status = "Config received".into(); } }, Message::FreqStartChanged(s) => self.freq_start = s, Message::FreqStopChanged(s) => self.freq_stop = s, Message::PpdChanged(s) => self.ppd = s, Message::RtiaSelected(r) => self.rtia = r, Message::RcalSelected(r) => self.rcal = r, Message::ApplySettings => { let fs = self.freq_start.parse::().unwrap_or(1000.0); let fe = self.freq_stop.parse::().unwrap_or(200000.0); let ppd = self.ppd.parse::().unwrap_or(10); self.send_cmd(&protocol::build_sysex_set_sweep(fs, fe, ppd)); self.send_cmd(&protocol::build_sysex_set_rtia(self.rtia)); self.send_cmd(&protocol::build_sysex_set_rcal(self.rcal)); self.send_cmd(&protocol::build_sysex_get_config()); } Message::StartSweep => { self.send_cmd(&protocol::build_sysex_start_sweep()); } } Task::none() } pub fn subscription(&self) -> Subscription { Subscription::run_with_id( "ble", iced::stream::channel(100, |mut output| async move { loop { let (ble_tx, mut ble_rx) = mpsc::unbounded_channel::(); let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::>(); let _ = output.send(Message::BleReady(cmd_tx)).await; let tx = ble_tx.clone(); tokio::spawn(async move { if let Err(e) = crate::ble::connect_and_run(tx, cmd_rx).await { eprintln!("BLE: {e}"); } }); while let Some(ev) = ble_rx.recv().await { let msg = match ev { BleEvent::Status(s) => Message::BleStatus(s), BleEvent::Data(m) => Message::BleData(m), }; let _ = output.send(msg).await; } let _ = output .send(Message::BleStatus("Reconnecting...".into())) .await; tokio::time::sleep(Duration::from_secs(2)).await; } }), ) } pub fn view(&self) -> Element<'_, Message> { let status = text(&self.status).size(16); let controls = row![ column![ text("Start Hz").size(12), text_input("1000", &self.freq_start) .on_input(Message::FreqStartChanged) .width(100), ] .spacing(2), column![ text("Stop Hz").size(12), text_input("200000", &self.freq_stop) .on_input(Message::FreqStopChanged) .width(100), ] .spacing(2), column![ text("PPD").size(12), text_input("10", &self.ppd) .on_input(Message::PpdChanged) .width(60), ] .spacing(2), column![ text("RTIA").size(12), pick_list(Rtia::ALL, Some(self.rtia), Message::RtiaSelected), ] .spacing(2), column![ text("RCAL").size(12), pick_list(Rcal::ALL, Some(self.rcal), Message::RcalSelected), ] .spacing(2), button("Apply").on_press(Message::ApplySettings), button("Sweep").on_press(Message::StartSweep), ] .spacing(10) .align_y(iced::Alignment::End); let header = row![ text("Freq (Hz)").width(100), text("|Z| (Ohm)").width(100), text("Phase (deg)").width(100), text("Re (Ohm)").width(100), text("Im (Ohm)").width(100), ] .spacing(10); let mut data_rows = column![header].spacing(4); for pt in &self.points { data_rows = data_rows.push( row![ text(format!("{:.1}", pt.freq_hz)).width(100), text(format!("{:.2}", pt.mag_ohms)).width(100), text(format!("{:.2}", pt.phase_deg)).width(100), text(format!("{:.2}", pt.z_real)).width(100), text(format!("{:.2}", pt.z_imag)).width(100), ] .spacing(10), ); } let bode = canvas(crate::plot::BodePlot { points: &self.points }) .width(Length::FillPortion(3)) .height(300); let nyquist = canvas(crate::plot::NyquistPlot { points: &self.points }) .width(Length::FillPortion(2)) .height(300); let plots = row![bode, nyquist].spacing(10); let content = column![status, controls, plots, scrollable(data_rows)] .spacing(20) .padding(20); container(content) .width(Length::Fill) .height(Length::Fill) .into() } }