349 lines
8.7 KiB
Swift
349 lines
8.7 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|