import Foundation import Observation enum Tab: String, CaseIterable, Identifiable { case eis = "EIS" case lsv = "LSV" case amp = "Amperometry" case chlorine = "Chlorine" case ph = "pH" case sessions = "Sessions" var id: String { rawValue } } // MARK: - Enums mirroring protocol.rs enum Rtia: UInt8, CaseIterable, Identifiable { case r200 = 0, r1k, r5k, r10k, r20k, r40k, r80k, r160k, extDe0 var id: UInt8 { rawValue } var label: String { switch self { case .r200: "200\u{2126}" case .r1k: "1k\u{2126}" case .r5k: "5k\u{2126}" case .r10k: "10k\u{2126}" case .r20k: "20k\u{2126}" case .r40k: "40k\u{2126}" case .r80k: "80k\u{2126}" case .r160k: "160k\u{2126}" case .extDe0: "Ext 3k\u{2126} (DE0)" } } } enum Rcal: UInt8, CaseIterable, Identifiable { case r200 = 0, r3k var id: UInt8 { rawValue } var label: String { switch self { case .r200: "200\u{2126} (RCAL0-RCAL1)" case .r3k: "3k\u{2126} (RCAL0-AIN0)" } } } enum Electrode: UInt8, CaseIterable, Identifiable { case fourWire = 0, threeWire var id: UInt8 { rawValue } var label: String { switch self { case .fourWire: "4-wire (AIN)" case .threeWire: "3-wire (CE0/RE0/SE0)" } } } enum LpRtia: UInt8, CaseIterable, Identifiable { case r200 = 0, r1k, r2k, r3k, r4k, r6k, r8k, r10k, r12k, r16k case r20k, r24k, r30k, r32k, r40k, r48k, r64k, r85k, r96k case r100k, r120k, r128k, r160k, r196k, r256k, r512k var id: UInt8 { rawValue } var label: String { switch self { case .r200: "200\u{2126}" case .r1k: "1k\u{2126}" case .r2k: "2k\u{2126}" case .r3k: "3k\u{2126}" case .r4k: "4k\u{2126}" case .r6k: "6k\u{2126}" case .r8k: "8k\u{2126}" case .r10k: "10k\u{2126}" case .r12k: "12k\u{2126}" case .r16k: "16k\u{2126}" case .r20k: "20k\u{2126}" case .r24k: "24k\u{2126}" case .r30k: "30k\u{2126}" case .r32k: "32k\u{2126}" case .r40k: "40k\u{2126}" case .r48k: "48k\u{2126}" case .r64k: "64k\u{2126}" case .r85k: "85k\u{2126}" case .r96k: "96k\u{2126}" case .r100k: "100k\u{2126}" case .r120k: "120k\u{2126}" case .r128k: "128k\u{2126}" case .r160k: "160k\u{2126}" case .r196k: "196k\u{2126}" case .r256k: "256k\u{2126}" case .r512k: "512k\u{2126}" } } } // MARK: - Data types mirroring protocol.rs struct EisPoint: Identifiable { let id = UUID() var freqHz: Float var magOhms: Float var phaseDeg: Float var zReal: Float var zImag: Float var rtiaMagBefore: Float var rtiaMagAfter: Float var revMag: Float var revPhase: Float var pctErr: Float } struct LsvPoint: Identifiable { let id = UUID() var vMv: Float var iUa: Float } struct AmpPoint: Identifiable { let id = UUID() var tMs: Float var iUa: Float } struct ClPoint: Identifiable { let id = UUID() var tMs: Float var iUa: Float var phase: UInt8 } struct ClResult { var iFreeUa: Float var iTotalUa: Float } struct PhResult { var vOcpMv: Float var ph: Float var tempC: Float } // MARK: - App State @Observable final class AppState { var tab: Tab = .eis var status: String = "Disconnected" var bleConnected: Bool = false var tempC: Float = 25.0 // EIS var eisPoints: [EisPoint] = [] var sweepTotal: UInt16 = 0 var freqStart: String = "1000" var freqStop: String = "200000" var ppd: String = "10" var rtia: Rtia = .r5k var rcal: Rcal = .r3k var electrode: Electrode = .fourWire // LSV var lsvPoints: [LsvPoint] = [] var lsvTotal: UInt16 = 0 var lsvStartV: String = "0" var lsvStopV: String = "500" var lsvScanRate: String = "50" var lsvRtia: LpRtia = .r10k // Amperometry var ampPoints: [AmpPoint] = [] var ampTotal: UInt16 = 0 var ampRunning: Bool = false var ampVHold: String = "200" var ampInterval: String = "100" var ampDuration: String = "60" var ampRtia: LpRtia = .r10k // Chlorine var clPoints: [ClPoint] = [] var clResult: ClResult? = nil var clTotal: UInt16 = 0 var clCondV: String = "800" var clCondT: String = "2000" var clFreeV: String = "100" var clTotalV: String = "-200" var clDepT: String = "5000" var clMeasT: String = "5000" var clRtia: LpRtia = .r10k // pH var phResult: PhResult? = nil var phStabilize: String = "30" // Reference baselines var eisRef: [EisPoint]? = nil var lsvRef: [LsvPoint]? = nil var ampRef: [AmpPoint]? = nil var clRef: (points: [ClPoint], result: ClResult)? = nil var phRef: PhResult? = nil // Device reference collection var collectingRefs: Bool = false var hasDeviceRefs: Bool = false // Clean var cleanV: String = "1200" var cleanDur: String = "30" // MARK: - Actions func applyEISSettings() { let fs = Float(freqStart) ?? 1000 let fe = Float(freqStop) ?? 200000 let p = UInt16(ppd) ?? 10 status = "Applying: \(fs)-\(fe) Hz, \(p) PPD" // BLEManager sends: set_sweep, set_rtia, set_rcal, set_electrode, get_config } func startSweep() { eisPoints.removeAll() status = "Starting sweep..." // BLEManager sends: get_temp, start_sweep } func startLSV() { lsvPoints.removeAll() let vs = Float(lsvStartV) ?? 0 let ve = Float(lsvStopV) ?? 500 status = "Starting LSV: \(vs)-\(ve) mV" // BLEManager sends: get_temp, start_lsv } func startAmp() { ampPoints.removeAll() ampRunning = true status = "Starting amperometry..." // BLEManager sends: get_temp, start_amp } func stopAmp() { ampRunning = false status = "Stopping amperometry..." // BLEManager sends: stop_amp } func startChlorine() { clPoints.removeAll() clResult = nil status = "Starting chlorine measurement..." // BLEManager sends: get_temp, start_cl } func startPh() { phResult = nil status = "Starting pH measurement..." // BLEManager sends: get_temp, start_ph } func setReference() { switch tab { case .eis where !eisPoints.isEmpty: eisRef = eisPoints status = "EIS reference set (\(eisPoints.count) pts)" case .lsv where !lsvPoints.isEmpty: lsvRef = lsvPoints status = "LSV reference set (\(lsvPoints.count) pts)" case .amp where !ampPoints.isEmpty: ampRef = ampPoints status = "Amp reference set (\(ampPoints.count) pts)" case .chlorine where !clPoints.isEmpty: if let r = clResult { clRef = (clPoints, r) status = "Chlorine reference set" } case .ph: if let r = phResult { phRef = r status = String(format: "pH reference set (%.2f)", r.ph) } default: break } } func clearReference() { switch tab { case .eis: eisRef = nil; status = "EIS reference cleared" case .lsv: lsvRef = nil; status = "LSV reference cleared" case .amp: ampRef = nil; status = "Amp reference cleared" case .chlorine: clRef = nil; status = "Chlorine reference cleared" case .ph: phRef = nil; status = "pH reference cleared" case .sessions: break } } func collectRefs() { collectingRefs = true status = "Starting reference collection..." // BLEManager sends: start_refs } func clearRefs() { collectingRefs = false hasDeviceRefs = false eisRef = nil lsvRef = nil ampRef = nil clRef = nil phRef = nil status = "Refs cleared" // BLEManager sends: clear_refs } func startClean() { let v = Float(cleanV) ?? 1200 let d = Float(cleanDur) ?? 30 status = String(format: "Cleaning: %.0f mV for %.0fs", v, d) // BLEManager sends: start_clean } var hasCurrentRef: Bool { switch tab { case .eis: eisRef != nil case .lsv: lsvRef != nil case .amp: ampRef != nil case .chlorine: clRef != nil case .ph: phRef != nil case .sessions: false } } var hasCurrentData: Bool { switch tab { case .eis: !eisPoints.isEmpty case .lsv: !lsvPoints.isEmpty case .amp: !ampPoints.isEmpty case .chlorine: clResult != nil case .ph: phResult != nil case .sessions: false } } }