iOS: add pH calibration UI in Calibrate tab
This commit is contained in:
parent
818c4ff7a2
commit
1441c5ec42
|
|
@ -15,6 +15,7 @@ struct CalibrateView: View {
|
||||||
resultsSection
|
resultsSection
|
||||||
cellConstantSection
|
cellConstantSection
|
||||||
chlorineCalSection
|
chlorineCalSection
|
||||||
|
phCalibrationSection
|
||||||
}
|
}
|
||||||
.navigationTitle("Calibrate")
|
.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
|
// MARK: - Calculations
|
||||||
|
|
||||||
private func saltGrams(volumeGal: Double, ppm: Double) -> Double {
|
private func saltGrams(volumeGal: Double, ppm: Double) -> Double {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue