243 lines
8.2 KiB
Rust
243 lines
8.2 KiB
Rust
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<Vec<u8>>),
|
|
BleStatus(String),
|
|
BleData(EisMessage),
|
|
FreqStartChanged(String),
|
|
FreqStopChanged(String),
|
|
PpdChanged(String),
|
|
RtiaSelected(Rtia),
|
|
RcalSelected(Rcal),
|
|
ApplySettings,
|
|
StartSweep,
|
|
}
|
|
|
|
pub struct App {
|
|
status: String,
|
|
points: Vec<EisPoint>,
|
|
sweep_total: u16,
|
|
|
|
freq_start: String,
|
|
freq_stop: String,
|
|
ppd: String,
|
|
rtia: Rtia,
|
|
rcal: Rcal,
|
|
|
|
cmd_tx: Option<mpsc::UnboundedSender<Vec<u8>>>,
|
|
}
|
|
|
|
impl App {
|
|
pub fn new() -> (Self, Task<Message>) {
|
|
(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<Message> {
|
|
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::<f32>().unwrap_or(1000.0);
|
|
let fe = self.freq_stop.parse::<f32>().unwrap_or(200000.0);
|
|
let ppd = self.ppd.parse::<u16>().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<Message> {
|
|
Subscription::run_with_id(
|
|
"ble",
|
|
iced::stream::channel(100, |mut output| async move {
|
|
loop {
|
|
let (ble_tx, mut ble_rx) =
|
|
mpsc::unbounded_channel::<BleEvent>();
|
|
let (cmd_tx, cmd_rx) =
|
|
mpsc::unbounded_channel::<Vec<u8>>();
|
|
|
|
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()
|
|
}
|
|
}
|