EIS-BLE-S3/cue-ios/CueIOS/Views/CalibrateView.swift

188 lines
6.3 KiB
Swift

import SwiftUI
struct CalibrateView: View {
@Bindable var state: AppState
private var volumeGal: Double { state.calVolumeGal }
private var naclPpm: Double { Double(state.calNaclPpm) ?? 0 }
private var clPpm: Double { Double(state.calClPpm) ?? 0 }
private var bleachPct: Double { Double(state.calBleachPct) ?? 0 }
private var tempC: Double { Double(state.calTempC) ?? 25 }
var body: some View {
Form {
inputSection
resultsSection
cellConstantSection
chlorineCalSection
}
.navigationTitle("Calibrate")
}
// MARK: - Inputs
private var inputSection: some View {
Section("Solution Parameters") {
Stepper("Volume: \(Int(state.calVolumeGal)) gal",
value: $state.calVolumeGal, in: 5...30, step: 5)
HStack {
Text("NaCl ppm")
Spacer()
TextField("ppm", text: $state.calNaclPpm)
.multilineTextAlignment(.trailing)
.frame(width: 80)
#if os(iOS)
.keyboardType(.decimalPad)
#endif
}
HStack {
Text("Free Cl ppm")
Spacer()
TextField("ppm", text: $state.calClPpm)
.multilineTextAlignment(.trailing)
.frame(width: 80)
#if os(iOS)
.keyboardType(.decimalPad)
#endif
}
HStack {
Text("Bleach %")
Spacer()
TextField("%", text: $state.calBleachPct)
.multilineTextAlignment(.trailing)
.frame(width: 80)
#if os(iOS)
.keyboardType(.decimalPad)
#endif
}
HStack {
Text("Temperature")
Spacer()
TextField("\u{00B0}C", text: $state.calTempC)
.multilineTextAlignment(.trailing)
.frame(width: 80)
#if os(iOS)
.keyboardType(.decimalPad)
#endif
}
}
}
// MARK: - Computed results
private var resultsSection: some View {
Section("Preparation") {
let salt = saltGrams(volumeGal: volumeGal, ppm: naclPpm)
let tbsp = salt / 17.0
Text(String(format: "Salt: %.1f g (%.1f tbsp)", salt, tbsp))
let bleach = bleachMl(volumeGal: volumeGal, clPpm: clPpm, bleachPct: bleachPct)
let tsp = bleach / 5.0
Text(String(format: "Bleach: %.1f mL (%.1f tsp)", bleach, tsp))
let kappa = theoreticalConductivity(naclPpm: naclPpm, tempC: tempC)
Text(String(format: "Theoretical \u{03BA}: %.2f mS/cm at %.0f\u{00B0}C", kappa, tempC))
}
}
// MARK: - Cell constant from EIS
private var cellConstantSection: some View {
Section("Cell Constant") {
Button("Calculate K from Last Sweep") {
guard let rs = extractRs(points: state.eisPoints) else {
state.status = "No valid EIS data for Rs"
return
}
let kappa = theoreticalConductivity(naclPpm: naclPpm, tempC: tempC)
let k = cellConstant(kappaMsCm: kappa, rsOhm: Double(rs))
state.calRs = Double(rs)
state.calCellConstant = k
state.send(buildSysexSetCellK(Float(k)))
state.status = String(format: "K = %.4f cm\u{207B}\u{00B9} (Rs = %.1f \u{2126})", k, rs)
}
.disabled(state.eisPoints.isEmpty)
if let rs = state.calRs {
Text(String(format: "Rs: %.1f \u{2126}", rs))
}
if let k = state.calCellConstant {
Text(String(format: "Cell constant K: %.4f cm\u{207B}\u{00B9}", k))
}
}
}
// MARK: - Chlorine calibration
private var chlorineCalSection: some View {
Section("Chlorine Calibration") {
if let f = state.clFactor {
Text(String(format: "Cl factor: %.6f ppm/\u{00B5}A", f))
}
if let r = state.clResult {
Text(String(format: "Last free Cl peak: %.3f \u{00B5}A", r.iFreeUa))
}
HStack {
Text("Known Cl ppm")
Spacer()
TextField("ppm", text: $state.clCalKnownPpm)
.multilineTextAlignment(.trailing)
.frame(width: 80)
#if os(iOS)
.keyboardType(.decimalPad)
#endif
}
Button("Set Cl Factor") {
guard let r = state.clResult else {
state.status = "No chlorine measurement"
return
}
let knownPpm = Double(state.clCalKnownPpm) ?? 0
let peak = abs(Double(r.iFreeUa))
guard peak > 0 else {
state.status = "Peak current is zero"
return
}
let factor = knownPpm / peak
state.clFactor = factor
state.send(buildSysexSetClFactor(Float(factor)))
state.status = String(format: "Cl factor: %.6f ppm/\u{00B5}A", factor)
}
.disabled(state.clResult == nil)
}
}
// MARK: - Calculations
private func saltGrams(volumeGal: Double, ppm: Double) -> Double {
let liters = volumeGal * 3.78541
return ppm * liters / 1000.0
}
private func bleachMl(volumeGal: Double, clPpm: Double, bleachPct: Double) -> Double {
let liters = volumeGal * 3.78541
let clNeededMg = clPpm * liters
let bleachMgPerMl = bleachPct * 10.0
return clNeededMg / bleachMgPerMl
}
private func theoreticalConductivity(naclPpm: Double, tempC: Double) -> Double {
let kappa25 = naclPpm * 2.0 / 1000.0
return kappa25 * (1.0 + 0.0212 * (tempC - 25.0))
}
private func extractRs(points: [EisPoint]) -> Float? {
points.map(\.zReal).filter { $0.isFinite && $0 > 0 }.min()
}
private func cellConstant(kappaMsCm: Double, rsOhm: Double) -> Double {
(kappaMsCm / 1000.0) * rsOhm
}
}