diff --git a/cue-ios/CueIOS/Views/CalibrateView.swift b/cue-ios/CueIOS/Views/CalibrateView.swift index 23486a3..3b75f3f 100644 --- a/cue-ios/CueIOS/Views/CalibrateView.swift +++ b/cue-ios/CueIOS/Views/CalibrateView.swift @@ -15,6 +15,7 @@ struct CalibrateView: View { resultsSection cellConstantSection chlorineCalSection + phCalibrationSection } .navigationTitle("Calibrate") } @@ -158,6 +159,80 @@ struct CalibrateView: View { } } + // MARK: - pH calibration + + private var phCalibrationSection: some View { + Section("pH Calibration (Q/HQ peak-shift)") { + if let s = state.phSlope, let o = state.phOffset { + Text(String(format: "slope: %.4f mV/pH offset: %.4f mV", s, o)) + if let peak = detectQhqPeak(state.lsvPoints) { + if abs(s) > 1e-6 { + let ph = (Double(peak) - o) / s + Text(String(format: "Computed pH: %.2f (peak at %.1f mV)", ph, peak)) + } + } + } + + HStack { + Text("Known pH") + Spacer() + TextField("7.00", text: $state.phCalKnown) + .multilineTextAlignment(.trailing) + .frame(width: 80) + #if os(iOS) + .keyboardType(.decimalPad) + #endif + } + + Button("Add Calibration Point") { + guard let peak = detectQhqPeak(state.lsvPoints) else { + state.status = "No Q/HQ peak found in LSV data" + return + } + guard let ph = Double(state.phCalKnown) else { return } + state.phCalPoints.append((ph: ph, mV: Double(peak))) + state.status = String(format: "pH cal point: pH=%.2f peak=%.1f mV (%d pts)", + ph, peak, state.phCalPoints.count) + } + .disabled(state.lsvPoints.isEmpty) + + ForEach(Array(state.phCalPoints.enumerated()), id: \.offset) { i, pt in + Text(String(format: "%d. pH=%.2f peak=%.1f mV", i + 1, pt.ph, pt.mV)) + .font(.caption) + } + + Button("Clear Points") { + state.phCalPoints.removeAll() + state.status = "pH cal points cleared" + } + .disabled(state.phCalPoints.isEmpty) + + Button("Compute & Set pH Cal") { + let pts = state.phCalPoints + guard pts.count >= 2 else { + state.status = "Need at least 2 calibration points" + return + } + let n = Double(pts.count) + let meanPh = pts.map(\.ph).reduce(0, +) / n + let meanV = pts.map(\.mV).reduce(0, +) / n + let num = pts.map { ($0.ph - meanPh) * ($0.mV - meanV) }.reduce(0, +) + let den = pts.map { ($0.ph - meanPh) * ($0.ph - meanPh) }.reduce(0, +) + guard abs(den) > 1e-12 else { + state.status = "Degenerate calibration data" + return + } + let slope = num / den + let offset = meanV - slope * meanPh + state.phSlope = slope + state.phOffset = offset + state.send(buildSysexSetPhCal(Float(slope), Float(offset))) + state.status = String(format: "pH cal set: slope=%.4f offset=%.4f", slope, offset) + } + .disabled(state.phCalPoints.count < 2) + } + } + // MARK: - Calculations private func saltGrams(volumeGal: Double, ppm: Double) -> Double {