diff --git a/cue/src/app.rs b/cue/src/app.rs index bfd4889..e0e0caa 100644 --- a/cue/src/app.rs +++ b/cue/src/app.rs @@ -998,6 +998,45 @@ impl App { self.status = "No valid EIS data for Rs extraction".into(); } } + Message::PhCalKnownChanged(s) => { self.ph_cal_known = s; } + Message::PhAddCalPoint => { + if let Some(peak_mv) = crate::lsv_analysis::detect_qhq_peak(&self.lsv_points) { + if let Ok(ph) = self.ph_cal_known.parse::() { + self.ph_cal_points.push((ph, peak_mv)); + self.status = format!("pH cal point: pH={:.2} peak={:.1} mV ({} pts)", + ph, peak_mv, self.ph_cal_points.len()); + } + } else { + self.status = "No Q/HQ peak found in LSV data".into(); + } + } + Message::PhClearCalPoints => { + self.ph_cal_points.clear(); + self.status = "pH cal points cleared".into(); + } + Message::PhComputeAndSetCal => { + if self.ph_cal_points.len() < 2 { + self.status = "Need at least 2 calibration points".into(); + } else { + let n = self.ph_cal_points.len() as f32; + let mean_ph: f32 = self.ph_cal_points.iter().map(|p| p.0).sum::() / n; + let mean_v: f32 = self.ph_cal_points.iter().map(|p| p.1).sum::() / n; + let num: f32 = self.ph_cal_points.iter() + .map(|p| (p.0 - mean_ph) * (p.1 - mean_v)).sum(); + let den: f32 = self.ph_cal_points.iter() + .map(|p| (p.0 - mean_ph).powi(2)).sum(); + if den.abs() < 1e-12 { + self.status = "Degenerate calibration data".into(); + } else { + let slope = num / den; + let offset = mean_v - slope * mean_ph; + self.ph_slope = Some(slope); + self.ph_offset = Some(offset); + self.send_cmd(&protocol::build_sysex_set_ph_cal(slope, offset)); + self.status = format!("pH cal set: slope={:.4} offset={:.4}", slope, offset); + } + } + } Message::ClCalKnownPpmChanged(s) => { self.cl_cal_known_ppm = s; } Message::ClSetFactor => { let known_ppm = self.cl_cal_known_ppm.parse::().unwrap_or(0.0); @@ -1750,6 +1789,47 @@ impl App { ].spacing(10).align_y(iced::Alignment::End) ); + /* pH calibration */ + results = results.push(iced::widget::horizontal_rule(1)); + results = results.push(text("pH Calibration (Q/HQ peak-shift)").size(16)); + if let (Some(s), Some(o)) = (self.ph_slope, self.ph_offset) { + results = results.push(text(format!("slope: {:.4} mV/pH offset: {:.4} mV", s, o)).size(14)); + if let Some(peak) = crate::lsv_analysis::detect_qhq_peak(&self.lsv_points) { + if s.abs() > 1e-6 { + let ph = (peak - o) / s; + results = results.push(text(format!("Computed pH: {:.2} (peak at {:.1} mV)", ph, peak)).size(14)); + } + } + } + results = results.push( + row![ + column![ + text("Known pH").size(12), + text_input("7.00", &self.ph_cal_known) + .on_input(Message::PhCalKnownChanged).width(80), + ].spacing(2), + button(text("Add Point").size(13)) + .style(style_action()) + .padding([6, 16]) + .on_press(Message::PhAddCalPoint), + ].spacing(10).align_y(iced::Alignment::End) + ); + for (i, (ph, mv)) in self.ph_cal_points.iter().enumerate() { + results = results.push(text(format!(" {}. pH={:.2} peak={:.1} mV", i + 1, ph, mv)).size(13)); + } + results = results.push( + row![ + button(text("Clear Points").size(13)) + .style(style_action()) + .padding([6, 16]) + .on_press(Message::PhClearCalPoints), + button(text("Compute & Set pH Cal").size(13)) + .style(style_action()) + .padding([6, 16]) + .on_press(Message::PhComputeAndSetCal), + ].spacing(10) + ); + row![ container(inputs).width(Length::FillPortion(2)), iced::widget::vertical_rule(1),