cue: measurement browser — load, compare, reference overlay from saved data
This commit is contained in:
parent
ae35b1248f
commit
b2493ffb54
450
cue/src/app.rs
450
cue/src/app.rs
|
|
@ -1,6 +1,7 @@
|
|||
use futures::SinkExt;
|
||||
use iced::widget::{
|
||||
button, canvas, column, container, pane_grid, pick_list, row, text, text_editor, text_input,
|
||||
button, canvas, column, container, pane_grid, pick_list, row, scrollable, text, text_editor,
|
||||
text_input,
|
||||
};
|
||||
use iced::widget::button::Style as ButtonStyle;
|
||||
use iced::{Border, Color, Element, Length, Subscription, Task, Theme};
|
||||
|
|
@ -14,7 +15,7 @@ use crate::protocol::{
|
|||
self, AmpPoint, ClPoint, ClResult, Electrode, EisMessage, EisPoint, LpRtia, LsvPoint,
|
||||
PhResult, Rcal, Rtia,
|
||||
};
|
||||
use crate::storage::{Session, Storage};
|
||||
use crate::storage::{self, Session, Storage};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Tab {
|
||||
|
|
@ -23,6 +24,7 @@ pub enum Tab {
|
|||
Amp,
|
||||
Chlorine,
|
||||
Ph,
|
||||
Browse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -107,6 +109,13 @@ pub enum Message {
|
|||
SelectSession(Option<i64>),
|
||||
DeleteSession,
|
||||
SessionNameInput(String),
|
||||
/* Browse */
|
||||
BrowseSelectSession(i64),
|
||||
BrowseSelectMeasurement(i64),
|
||||
BrowseLoadAsActive(i64),
|
||||
BrowseLoadAsReference(i64),
|
||||
BrowseDeleteMeasurement(i64),
|
||||
BrowseBack,
|
||||
/* Misc */
|
||||
OpenMidiSetup,
|
||||
RefreshMidi,
|
||||
|
|
@ -128,6 +137,13 @@ pub struct App {
|
|||
session_name_input: String,
|
||||
creating_session: bool,
|
||||
|
||||
/* Browse */
|
||||
browse_sessions: Vec<(Session, i64)>,
|
||||
browse_measurements: Vec<(storage::Measurement, i64)>,
|
||||
browse_selected_session: Option<i64>,
|
||||
browse_selected_measurement: Option<i64>,
|
||||
browse_preview: String,
|
||||
|
||||
/* EIS */
|
||||
eis_points: Vec<EisPoint>,
|
||||
sweep_total: u16,
|
||||
|
|
@ -324,6 +340,12 @@ impl App {
|
|||
session_name_input: String::new(),
|
||||
creating_session: false,
|
||||
|
||||
browse_sessions: Vec::new(),
|
||||
browse_measurements: Vec::new(),
|
||||
browse_selected_session: None,
|
||||
browse_selected_measurement: None,
|
||||
browse_preview: String::new(),
|
||||
|
||||
eis_points: Vec::new(),
|
||||
sweep_total: 0,
|
||||
freq_start: "1000".into(),
|
||||
|
|
@ -655,7 +677,23 @@ impl App {
|
|||
}
|
||||
}
|
||||
},
|
||||
Message::TabSelected(t) => self.tab = t,
|
||||
Message::TabSelected(t) => {
|
||||
if t == Tab::Browse {
|
||||
self.browse_sessions = self.storage.list_sessions()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let cnt = self.storage.measurement_count(s.id).unwrap_or(0);
|
||||
(s, cnt)
|
||||
})
|
||||
.collect();
|
||||
self.browse_selected_session = None;
|
||||
self.browse_selected_measurement = None;
|
||||
self.browse_measurements.clear();
|
||||
self.browse_preview.clear();
|
||||
}
|
||||
self.tab = t;
|
||||
}
|
||||
Message::PaneResized(event) => {
|
||||
self.panes.resize(event.split, event.ratio);
|
||||
}
|
||||
|
|
@ -666,7 +704,7 @@ impl App {
|
|||
Tab::Lsv => self.lsv_data.perform(action),
|
||||
Tab::Amp => self.amp_data.perform(action),
|
||||
Tab::Chlorine => self.cl_data.perform(action),
|
||||
Tab::Ph => {}
|
||||
Tab::Ph | Tab::Browse => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -819,6 +857,7 @@ impl App {
|
|||
Tab::Amp => { self.amp_ref = None; self.status = "Amp reference cleared".into(); }
|
||||
Tab::Chlorine => { self.cl_ref = None; self.status = "Chlorine reference cleared".into(); }
|
||||
Tab::Ph => { self.ph_ref = None; self.status = "pH reference cleared".into(); }
|
||||
Tab::Browse => {}
|
||||
}
|
||||
}
|
||||
/* Global */
|
||||
|
|
@ -875,6 +914,83 @@ impl App {
|
|||
Message::SessionNameInput(s) => {
|
||||
self.session_name_input = s;
|
||||
}
|
||||
/* Browse */
|
||||
Message::BrowseSelectSession(sid) => {
|
||||
self.browse_selected_session = Some(sid);
|
||||
self.browse_selected_measurement = None;
|
||||
self.browse_preview.clear();
|
||||
self.browse_measurements = self.storage.get_measurements(sid)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let cnt = self.storage.data_point_count(m.id).unwrap_or(0);
|
||||
(m, cnt)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
Message::BrowseSelectMeasurement(mid) => {
|
||||
self.browse_selected_measurement = Some(mid);
|
||||
let mtype = self.browse_measurements.iter()
|
||||
.find(|(m, _)| m.id == mid)
|
||||
.map(|(m, _)| m.mtype.clone());
|
||||
if let Some(mt) = mtype {
|
||||
let pts = self.storage.get_data_points(mid).unwrap_or_default();
|
||||
self.browse_preview = Self::format_preview(&mt, &pts);
|
||||
}
|
||||
}
|
||||
Message::BrowseLoadAsActive(mid) => {
|
||||
let mtype = self.browse_measurements.iter()
|
||||
.find(|(m, _)| m.id == mid)
|
||||
.map(|(m, _)| m.mtype.clone());
|
||||
if let Some(mt) = mtype {
|
||||
let pts = self.storage.get_data_points(mid).unwrap_or_default();
|
||||
self.load_measurement_active(&mt, &pts);
|
||||
}
|
||||
}
|
||||
Message::BrowseLoadAsReference(mid) => {
|
||||
let mtype = self.browse_measurements.iter()
|
||||
.find(|(m, _)| m.id == mid)
|
||||
.map(|(m, _)| m.mtype.clone());
|
||||
if let Some(mt) = mtype {
|
||||
let pts = self.storage.get_data_points(mid).unwrap_or_default();
|
||||
self.load_measurement_reference(&mt, &pts);
|
||||
}
|
||||
}
|
||||
Message::BrowseDeleteMeasurement(mid) => {
|
||||
let _ = self.storage.delete_measurement(mid);
|
||||
if self.browse_selected_measurement == Some(mid) {
|
||||
self.browse_selected_measurement = None;
|
||||
self.browse_preview.clear();
|
||||
}
|
||||
if let Some(sid) = self.browse_selected_session {
|
||||
self.browse_measurements = self.storage.get_measurements(sid)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let cnt = self.storage.data_point_count(m.id).unwrap_or(0);
|
||||
(m, cnt)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
self.browse_sessions = self.storage.list_sessions()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let cnt = self.storage.measurement_count(s.id).unwrap_or(0);
|
||||
(s, cnt)
|
||||
})
|
||||
.collect();
|
||||
self.status = "Measurement deleted".into();
|
||||
}
|
||||
Message::BrowseBack => {
|
||||
if self.browse_selected_measurement.is_some() {
|
||||
self.browse_selected_measurement = None;
|
||||
self.browse_preview.clear();
|
||||
} else {
|
||||
self.browse_selected_session = None;
|
||||
self.browse_measurements.clear();
|
||||
}
|
||||
}
|
||||
Message::OpenMidiSetup => {
|
||||
let _ = std::process::Command::new("open")
|
||||
.arg("-a")
|
||||
|
|
@ -948,6 +1064,7 @@ impl App {
|
|||
tab_btn("Amperometry", Tab::Amp, self.tab == Tab::Amp),
|
||||
tab_btn("Chlorine", Tab::Chlorine, self.tab == Tab::Chlorine),
|
||||
tab_btn("pH", Tab::Ph, self.tab == Tab::Ph),
|
||||
tab_btn("Browse", Tab::Browse, self.tab == Tab::Browse),
|
||||
button(text("MIDI Setup").size(13))
|
||||
.style(style_neutral())
|
||||
.padding([6, 14])
|
||||
|
|
@ -970,6 +1087,7 @@ impl App {
|
|||
Tab::Amp => self.amp_ref.is_some(),
|
||||
Tab::Chlorine => self.cl_ref.is_some(),
|
||||
Tab::Ph => self.ph_ref.is_some(),
|
||||
Tab::Browse => false,
|
||||
};
|
||||
let has_data = match self.tab {
|
||||
Tab::Eis => !self.eis_points.is_empty(),
|
||||
|
|
@ -977,6 +1095,7 @@ impl App {
|
|||
Tab::Amp => !self.amp_points.is_empty(),
|
||||
Tab::Chlorine => self.cl_result.is_some(),
|
||||
Tab::Ph => self.ph_result.is_some(),
|
||||
Tab::Browse => false,
|
||||
};
|
||||
|
||||
let mut ref_row = row![].spacing(4).align_y(iced::Alignment::Center);
|
||||
|
|
@ -1051,6 +1170,8 @@ impl App {
|
|||
|
||||
let body: Element<'_, Message> = if self.show_sysinfo {
|
||||
self.view_sysinfo()
|
||||
} else if self.tab == Tab::Browse {
|
||||
self.view_browse_body()
|
||||
} else if self.tab == Tab::Ph {
|
||||
self.view_ph_body()
|
||||
} else {
|
||||
|
|
@ -1289,6 +1410,8 @@ impl App {
|
|||
.spacing(10)
|
||||
.align_y(iced::Alignment::End)
|
||||
.into(),
|
||||
|
||||
Tab::Browse => row![].into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1348,7 +1471,7 @@ impl App {
|
|||
column![result_text, plot].spacing(4).height(Length::Fill).into()
|
||||
}
|
||||
}
|
||||
Tab::Ph => text("").into(),
|
||||
Tab::Ph | Tab::Browse => text("").into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1358,7 +1481,7 @@ impl App {
|
|||
Tab::Lsv => &self.lsv_data,
|
||||
Tab::Amp => &self.amp_data,
|
||||
Tab::Chlorine => &self.cl_data,
|
||||
Tab::Ph => return text("").into(),
|
||||
Tab::Ph | Tab::Browse => return text("").into(),
|
||||
};
|
||||
text_editor(content)
|
||||
.on_action(Message::DataAction)
|
||||
|
|
@ -1412,4 +1535,319 @@ impl App {
|
|||
.center(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
/* ---- Browse tab ---- */
|
||||
|
||||
fn view_browse_body(&self) -> Element<'_, Message> {
|
||||
let left = self.view_browse_sessions();
|
||||
let right = self.view_browse_detail();
|
||||
row![
|
||||
container(left).width(Length::FillPortion(2)).height(Length::Fill),
|
||||
iced::widget::vertical_rule(1),
|
||||
container(right).width(Length::FillPortion(3)).height(Length::Fill),
|
||||
]
|
||||
.spacing(8)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_browse_sessions(&self) -> Element<'_, Message> {
|
||||
let mut items = column![
|
||||
text("Sessions").size(16),
|
||||
iced::widget::horizontal_rule(1),
|
||||
].spacing(4);
|
||||
|
||||
if self.browse_sessions.is_empty() {
|
||||
items = items.push(text("No saved sessions").size(13));
|
||||
}
|
||||
|
||||
for (session, mcount) in &self.browse_sessions {
|
||||
let selected = self.browse_selected_session == Some(session.id);
|
||||
let bg = if selected {
|
||||
Color::from_rgb(0.30, 0.40, 0.55)
|
||||
} else {
|
||||
Color::from_rgb(0.18, 0.18, 0.20)
|
||||
};
|
||||
let label = format!(
|
||||
"{} ({} meas) {}",
|
||||
session.name, mcount, &session.created_at[..10.min(session.created_at.len())]
|
||||
);
|
||||
let sid = session.id;
|
||||
items = items.push(
|
||||
button(text(label).size(12))
|
||||
.style(btn_style(bg, Color::WHITE))
|
||||
.padding([6, 10])
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::BrowseSelectSession(sid)),
|
||||
);
|
||||
}
|
||||
|
||||
scrollable(items.width(Length::Fill))
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_browse_detail(&self) -> Element<'_, Message> {
|
||||
let Some(sid) = self.browse_selected_session else {
|
||||
return column![
|
||||
text("Select a session").size(14),
|
||||
].spacing(8).into();
|
||||
};
|
||||
|
||||
let session_name = self.browse_sessions.iter()
|
||||
.find(|(s, _)| s.id == sid)
|
||||
.map(|(s, _)| s.name.as_str())
|
||||
.unwrap_or("?");
|
||||
|
||||
let mut header = row![
|
||||
button(text("Back").size(11))
|
||||
.style(style_neutral())
|
||||
.padding([4, 10])
|
||||
.on_press(Message::BrowseBack),
|
||||
text(format!("Measurements in: {}", session_name)).size(14),
|
||||
].spacing(8).align_y(iced::Alignment::Center);
|
||||
|
||||
if let Some(mid) = self.browse_selected_measurement {
|
||||
header = header.push(iced::widget::horizontal_space());
|
||||
header = header.push(
|
||||
button(text("Load").size(11))
|
||||
.style(style_action())
|
||||
.padding([4, 12])
|
||||
.on_press(Message::BrowseLoadAsActive(mid)),
|
||||
);
|
||||
header = header.push(
|
||||
button(text("Load as Ref").size(11))
|
||||
.style(style_apply())
|
||||
.padding([4, 12])
|
||||
.on_press(Message::BrowseLoadAsReference(mid)),
|
||||
);
|
||||
header = header.push(
|
||||
button(text("Delete").size(11))
|
||||
.style(style_danger())
|
||||
.padding([4, 10])
|
||||
.on_press(Message::BrowseDeleteMeasurement(mid)),
|
||||
);
|
||||
}
|
||||
|
||||
let mut mlist = column![].spacing(2);
|
||||
|
||||
if self.browse_measurements.is_empty() {
|
||||
mlist = mlist.push(text("No measurements").size(13));
|
||||
}
|
||||
|
||||
for (m, pt_count) in &self.browse_measurements {
|
||||
let selected = self.browse_selected_measurement == Some(m.id);
|
||||
let bg = if selected {
|
||||
Color::from_rgb(0.30, 0.40, 0.55)
|
||||
} else {
|
||||
Color::from_rgb(0.18, 0.18, 0.20)
|
||||
};
|
||||
let type_label = match m.mtype.as_str() {
|
||||
"eis" => "EIS",
|
||||
"lsv" => "LSV",
|
||||
"amp" => "Amp",
|
||||
"chlorine" => "Cl",
|
||||
"ph" => "pH",
|
||||
other => other,
|
||||
};
|
||||
let ts = if m.created_at.len() > 10 { &m.created_at[11..] } else { &m.created_at };
|
||||
let label = format!("[{}] {} {} pts", type_label, ts, pt_count);
|
||||
let mid = m.id;
|
||||
mlist = mlist.push(
|
||||
button(text(label).size(12))
|
||||
.style(btn_style(bg, Color::WHITE))
|
||||
.padding([5, 10])
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::BrowseSelectMeasurement(mid)),
|
||||
);
|
||||
}
|
||||
|
||||
let mut body = column![
|
||||
header,
|
||||
iced::widget::horizontal_rule(1),
|
||||
].spacing(6);
|
||||
|
||||
body = body.push(
|
||||
scrollable(mlist.width(Length::Fill))
|
||||
.height(if self.browse_preview.is_empty() { Length::Fill } else { Length::FillPortion(2) }),
|
||||
);
|
||||
|
||||
if !self.browse_preview.is_empty() {
|
||||
body = body.push(iced::widget::horizontal_rule(1));
|
||||
body = body.push(
|
||||
scrollable(
|
||||
text(&self.browse_preview)
|
||||
.size(12)
|
||||
.font(iced::Font::MONOSPACE)
|
||||
)
|
||||
.height(Length::FillPortion(3)),
|
||||
);
|
||||
}
|
||||
|
||||
body.height(Length::Fill).into()
|
||||
}
|
||||
|
||||
fn format_preview(mtype: &str, pts: &[storage::DataPoint]) -> String {
|
||||
match mtype {
|
||||
"eis" => {
|
||||
let decoded: Vec<EisPoint> = pts.iter()
|
||||
.filter_map(|dp| serde_json::from_str(&dp.data_json).ok())
|
||||
.collect();
|
||||
fmt_eis(&decoded)
|
||||
}
|
||||
"lsv" => {
|
||||
let decoded: Vec<LsvPoint> = pts.iter()
|
||||
.filter_map(|dp| serde_json::from_str(&dp.data_json).ok())
|
||||
.collect();
|
||||
fmt_lsv(&decoded)
|
||||
}
|
||||
"amp" => {
|
||||
let decoded: Vec<AmpPoint> = pts.iter()
|
||||
.filter_map(|dp| serde_json::from_str(&dp.data_json).ok())
|
||||
.collect();
|
||||
fmt_amp(&decoded)
|
||||
}
|
||||
"chlorine" => {
|
||||
let decoded: Vec<ClPoint> = pts.iter()
|
||||
.filter_map(|dp| serde_json::from_str(&dp.data_json).ok())
|
||||
.collect();
|
||||
let mut s = fmt_cl(&decoded);
|
||||
if let Some(last) = pts.last()
|
||||
&& let Ok(wrap) = serde_json::from_str::<serde_json::Value>(&last.data_json)
|
||||
&& let Some(r) = wrap.get("result")
|
||||
&& let Ok(cr) = serde_json::from_value::<ClResult>(r.clone())
|
||||
{
|
||||
let _ = writeln!(s, "\nFree: {:.3} uA Total: {:.3} uA",
|
||||
cr.i_free_ua, cr.i_total_ua);
|
||||
}
|
||||
s
|
||||
}
|
||||
"ph" => {
|
||||
if let Some(dp) = pts.first() {
|
||||
if let Ok(r) = serde_json::from_str::<PhResult>(&dp.data_json) {
|
||||
format!("pH: {:.2} OCP: {:.1} mV Temp: {:.1} C", r.ph, r.v_ocp_mv, r.temp_c)
|
||||
} else {
|
||||
dp.data_json.clone()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
_ => format!("{} data points", pts.len()),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_measurement_active(&mut self, mtype: &str, pts: &[storage::DataPoint]) {
|
||||
match mtype {
|
||||
"eis" => {
|
||||
let decoded: Vec<EisPoint> = pts.iter()
|
||||
.filter_map(|dp| serde_json::from_str(&dp.data_json).ok())
|
||||
.collect();
|
||||
self.eis_data = text_editor::Content::with_text(&fmt_eis(&decoded));
|
||||
self.eis_points = decoded;
|
||||
self.tab = Tab::Eis;
|
||||
self.status = format!("Loaded EIS: {} pts", self.eis_points.len());
|
||||
}
|
||||
"lsv" => {
|
||||
let decoded: Vec<LsvPoint> = pts.iter()
|
||||
.filter_map(|dp| serde_json::from_str(&dp.data_json).ok())
|
||||
.collect();
|
||||
self.lsv_data = text_editor::Content::with_text(&fmt_lsv(&decoded));
|
||||
self.lsv_points = decoded;
|
||||
self.tab = Tab::Lsv;
|
||||
self.status = format!("Loaded LSV: {} pts", self.lsv_points.len());
|
||||
}
|
||||
"amp" => {
|
||||
let decoded: Vec<AmpPoint> = pts.iter()
|
||||
.filter_map(|dp| serde_json::from_str(&dp.data_json).ok())
|
||||
.collect();
|
||||
self.amp_data = text_editor::Content::with_text(&fmt_amp(&decoded));
|
||||
self.amp_points = decoded;
|
||||
self.tab = Tab::Amp;
|
||||
self.status = format!("Loaded Amp: {} pts", self.amp_points.len());
|
||||
}
|
||||
"chlorine" => {
|
||||
let cl_pts: Vec<ClPoint> = pts.iter()
|
||||
.filter_map(|dp| serde_json::from_str(&dp.data_json).ok())
|
||||
.collect();
|
||||
let mut result: Option<ClResult> = None;
|
||||
if let Some(last) = pts.last()
|
||||
&& let Ok(wrap) = serde_json::from_str::<serde_json::Value>(&last.data_json)
|
||||
&& let Some(r) = wrap.get("result")
|
||||
{
|
||||
result = serde_json::from_value(r.clone()).ok();
|
||||
}
|
||||
self.cl_data = text_editor::Content::with_text(&fmt_cl(&cl_pts));
|
||||
self.cl_points = cl_pts;
|
||||
self.cl_result = result;
|
||||
self.tab = Tab::Chlorine;
|
||||
self.status = format!("Loaded Chlorine: {} pts", self.cl_points.len());
|
||||
}
|
||||
"ph" => {
|
||||
if let Some(dp) = pts.first()
|
||||
&& let Ok(r) = serde_json::from_str::<PhResult>(&dp.data_json)
|
||||
{
|
||||
self.status = format!("Loaded pH: {:.2}", r.ph);
|
||||
self.ph_result = Some(r);
|
||||
self.tab = Tab::Ph;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_measurement_reference(&mut self, mtype: &str, pts: &[storage::DataPoint]) {
|
||||
match mtype {
|
||||
"eis" => {
|
||||
let decoded: Vec<EisPoint> = pts.iter()
|
||||
.filter_map(|dp| serde_json::from_str(&dp.data_json).ok())
|
||||
.collect();
|
||||
self.status = format!("EIS ref loaded: {} pts", decoded.len());
|
||||
self.eis_ref = Some(decoded);
|
||||
self.tab = Tab::Eis;
|
||||
}
|
||||
"lsv" => {
|
||||
let decoded: Vec<LsvPoint> = pts.iter()
|
||||
.filter_map(|dp| serde_json::from_str(&dp.data_json).ok())
|
||||
.collect();
|
||||
self.status = format!("LSV ref loaded: {} pts", decoded.len());
|
||||
self.lsv_ref = Some(decoded);
|
||||
self.tab = Tab::Lsv;
|
||||
}
|
||||
"amp" => {
|
||||
let decoded: Vec<AmpPoint> = pts.iter()
|
||||
.filter_map(|dp| serde_json::from_str(&dp.data_json).ok())
|
||||
.collect();
|
||||
self.status = format!("Amp ref loaded: {} pts", decoded.len());
|
||||
self.amp_ref = Some(decoded);
|
||||
self.tab = Tab::Amp;
|
||||
}
|
||||
"chlorine" => {
|
||||
let cl_pts: Vec<ClPoint> = pts.iter()
|
||||
.filter_map(|dp| serde_json::from_str(&dp.data_json).ok())
|
||||
.collect();
|
||||
let mut result = ClResult { i_free_ua: 0.0, i_total_ua: 0.0 };
|
||||
if let Some(last) = pts.last()
|
||||
&& let Ok(wrap) = serde_json::from_str::<serde_json::Value>(&last.data_json)
|
||||
&& let Some(r) = wrap.get("result")
|
||||
&& let Ok(cr) = serde_json::from_value::<ClResult>(r.clone())
|
||||
{
|
||||
result = cr;
|
||||
}
|
||||
self.status = format!("Cl ref loaded: {} pts", cl_pts.len());
|
||||
self.cl_ref = Some((cl_pts, result));
|
||||
self.tab = Tab::Chlorine;
|
||||
}
|
||||
"ph" => {
|
||||
if let Some(dp) = pts.first()
|
||||
&& let Ok(r) = serde_json::from_str::<PhResult>(&dp.data_json)
|
||||
{
|
||||
self.status = format!("pH ref loaded: {:.2}", r.ph);
|
||||
self.ph_ref = Some(r);
|
||||
self.tab = Tab::Ph;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use rusqlite::{Connection, params};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Session {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
|
|
@ -9,6 +10,7 @@ pub struct Session {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Measurement {
|
||||
pub id: i64,
|
||||
pub session_id: i64,
|
||||
|
|
@ -18,6 +20,7 @@ pub struct Measurement {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct DataPoint {
|
||||
pub id: i64,
|
||||
pub measurement_id: i64,
|
||||
|
|
@ -120,6 +123,27 @@ impl Storage {
|
|||
rows.collect()
|
||||
}
|
||||
|
||||
pub fn measurement_count(&self, session_id: i64) -> Result<i64, rusqlite::Error> {
|
||||
self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM measurements WHERE session_id = ?1",
|
||||
params![session_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn data_point_count(&self, measurement_id: i64) -> Result<i64, rusqlite::Error> {
|
||||
self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM data_points WHERE measurement_id = ?1",
|
||||
params![measurement_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delete_measurement(&self, id: i64) -> Result<(), rusqlite::Error> {
|
||||
self.conn.execute("DELETE FROM measurements WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_data_points(&self, measurement_id: i64) -> Result<Vec<DataPoint>, rusqlite::Error> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, measurement_id, idx, data_json \
|
||||
|
|
|
|||
Loading…
Reference in New Issue