merge integration
This commit is contained in:
commit
f5dd536ca4
|
|
@ -8,6 +8,7 @@ enum Tab: String, CaseIterable, Identifiable {
|
||||||
case amp = "Amperometry"
|
case amp = "Amperometry"
|
||||||
case chlorine = "Chlorine"
|
case chlorine = "Chlorine"
|
||||||
case ph = "pH"
|
case ph = "pH"
|
||||||
|
case calibrate = "Calibrate"
|
||||||
case sessions = "Sessions"
|
case sessions = "Sessions"
|
||||||
case connection = "Connection"
|
case connection = "Connection"
|
||||||
|
|
||||||
|
|
@ -84,6 +85,15 @@ final class AppState {
|
||||||
// Session
|
// Session
|
||||||
var currentSessionId: Int64? = nil
|
var currentSessionId: Int64? = nil
|
||||||
|
|
||||||
|
// Calibration
|
||||||
|
var calVolumeGal: Double = 25
|
||||||
|
var calNaclPpm: String = "2500"
|
||||||
|
var calClPpm: String = "5"
|
||||||
|
var calBleachPct: String = "7.825"
|
||||||
|
var calTempC: String = "40"
|
||||||
|
var calCellConstant: Double? = nil
|
||||||
|
var calRs: Double? = nil
|
||||||
|
|
||||||
// Clean
|
// Clean
|
||||||
var cleanV: String = "1200"
|
var cleanV: String = "1200"
|
||||||
var cleanDur: String = "30"
|
var cleanDur: String = "30"
|
||||||
|
|
@ -361,7 +371,7 @@ final class AppState {
|
||||||
case .amp: ampRef = nil; status = "Amp reference cleared"
|
case .amp: ampRef = nil; status = "Amp reference cleared"
|
||||||
case .chlorine: clRef = nil; status = "Chlorine reference cleared"
|
case .chlorine: clRef = nil; status = "Chlorine reference cleared"
|
||||||
case .ph: phRef = nil; status = "pH reference cleared"
|
case .ph: phRef = nil; status = "pH reference cleared"
|
||||||
case .sessions, .connection: break
|
case .calibrate, .sessions, .connection: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -402,7 +412,7 @@ final class AppState {
|
||||||
case .amp: ampRef != nil
|
case .amp: ampRef != nil
|
||||||
case .chlorine: clRef != nil
|
case .chlorine: clRef != nil
|
||||||
case .ph: phRef != nil
|
case .ph: phRef != nil
|
||||||
case .sessions, .connection: false
|
case .calibrate, .sessions, .connection: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,7 +423,7 @@ final class AppState {
|
||||||
case .amp: !ampPoints.isEmpty
|
case .amp: !ampPoints.isEmpty
|
||||||
case .chlorine: clResult != nil
|
case .chlorine: clResult != nil
|
||||||
case .ph: phResult != nil
|
case .ph: phResult != nil
|
||||||
case .sessions, .connection: false
|
case .calibrate, .sessions, .connection: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
.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.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: - 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,9 @@ struct ContentView: View {
|
||||||
sidebarButton(.chlorine, "Chlorine", "drop.fill")
|
sidebarButton(.chlorine, "Chlorine", "drop.fill")
|
||||||
sidebarButton(.ph, "pH", "scalemass")
|
sidebarButton(.ph, "pH", "scalemass")
|
||||||
}
|
}
|
||||||
|
Section("Tools") {
|
||||||
|
sidebarButton(.calibrate, "Calibrate", "lines.measurement.horizontal")
|
||||||
|
}
|
||||||
Section("Data") {
|
Section("Data") {
|
||||||
sidebarButton(.sessions, "Sessions", "folder")
|
sidebarButton(.sessions, "Sessions", "folder")
|
||||||
sidebarButton(.connection, "Connection", "antenna.radiowaves.left.and.right")
|
sidebarButton(.connection, "Connection", "antenna.radiowaves.left.and.right")
|
||||||
|
|
@ -124,6 +127,10 @@ struct ContentView: View {
|
||||||
.tabItem { Label("pH", systemImage: "scalemass") }
|
.tabItem { Label("pH", systemImage: "scalemass") }
|
||||||
.tag(Tab.ph)
|
.tag(Tab.ph)
|
||||||
|
|
||||||
|
CalibrateView(state: state)
|
||||||
|
.tabItem { Label("Calibrate", systemImage: "lines.measurement.horizontal") }
|
||||||
|
.tag(Tab.calibrate)
|
||||||
|
|
||||||
SessionView(state: state)
|
SessionView(state: state)
|
||||||
.tabItem { Label("Sessions", systemImage: "folder") }
|
.tabItem { Label("Sessions", systemImage: "folder") }
|
||||||
.tag(Tab.sessions)
|
.tag(Tab.sessions)
|
||||||
|
|
@ -144,6 +151,7 @@ struct ContentView: View {
|
||||||
case .amp: AmpView(state: state)
|
case .amp: AmpView(state: state)
|
||||||
case .chlorine: ChlorineView(state: state)
|
case .chlorine: ChlorineView(state: state)
|
||||||
case .ph: PhView(state: state)
|
case .ph: PhView(state: state)
|
||||||
|
case .calibrate: CalibrateView(state: state)
|
||||||
case .sessions: SessionView(state: state)
|
case .sessions: SessionView(state: state)
|
||||||
case .connection: ConnectionView(state: state)
|
case .connection: ConnectionView(state: state)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue