EIS-BLE-S3/cue/src/app.rs

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()
}
}