618 lines
20 KiB
Swift
618 lines
20 KiB
Swift
import Foundation
|
|
import Observation
|
|
|
|
enum LsvDensityMode: String, CaseIterable, Identifiable {
|
|
case ptsPerMv = "pts/mV"
|
|
case ptsPerSec = "pts/s"
|
|
var id: String { rawValue }
|
|
}
|
|
|
|
enum Tab: String, CaseIterable, Identifiable {
|
|
case eis = "EIS"
|
|
case lsv = "LSV"
|
|
case amp = "Amperometry"
|
|
case chlorine = "Chlorine"
|
|
case ph = "pH"
|
|
case calibrate = "Calibrate"
|
|
case sessions = "Sessions"
|
|
case connection = "Connection"
|
|
|
|
var id: String { rawValue }
|
|
}
|
|
|
|
// MARK: - App State
|
|
|
|
@Observable
|
|
final class AppState {
|
|
let transport: UDPManager
|
|
var tab: Tab = .eis
|
|
var status: String = "Disconnected"
|
|
var tempC: Float = 25.0
|
|
|
|
var connected: Bool { transport.state == .connected }
|
|
|
|
// 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 lsvPeaks: [LsvPeak] = []
|
|
var lsvTotal: UInt16 = 0
|
|
var lsvStartV: String = "0"
|
|
var lsvStopV: String = "500"
|
|
var lsvScanRate: String = "50"
|
|
var lsvRtia: LpRtia = .r10K
|
|
var lsvDensityMode: LsvDensityMode = .ptsPerMv
|
|
var lsvDensity: String = "1"
|
|
|
|
// 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
|
|
var clManualPeaks: Bool = false
|
|
|
|
// 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
|
|
private var eisRefs: [Int: [EisPoint]] = [:]
|
|
private var refMode: UInt8?
|
|
private var refRtia: UInt8?
|
|
|
|
// Session
|
|
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
|
|
var clFactor: Double? = nil
|
|
var clCalKnownPpm: String = "5"
|
|
var phSlope: Double? = nil
|
|
var phOffset: Double? = nil
|
|
var phCalPoints: [(ph: Double, mV: Double)] = []
|
|
var phCalKnown: String = "7.00"
|
|
|
|
// Clean
|
|
var cleanV: String = "1200"
|
|
var cleanDur: String = "30"
|
|
|
|
init() {
|
|
transport = UDPManager()
|
|
transport.setMessageHandler { [weak self] msg in
|
|
self?.handleMessage(msg)
|
|
}
|
|
}
|
|
|
|
// MARK: - Send helper
|
|
|
|
func send(_ sysex: [UInt8]) {
|
|
transport.send(sysex)
|
|
}
|
|
|
|
// MARK: - Message handler
|
|
|
|
private func handleMessage(_ msg: EisMessage) {
|
|
switch msg {
|
|
|
|
case .sweepStart(let numPoints, let freqStart, let freqStop):
|
|
if collectingRefs {
|
|
eisPoints.removeAll()
|
|
sweepTotal = numPoints
|
|
} else {
|
|
eisPoints.removeAll()
|
|
sweepTotal = numPoints
|
|
status = String(format: "Sweep: %d pts, %.0f--%.0f Hz", numPoints, freqStart, freqStop)
|
|
}
|
|
|
|
case .dataPoint(_, let point):
|
|
eisPoints.append(point)
|
|
if !collectingRefs {
|
|
status = "Receiving: \(eisPoints.count)/\(sweepTotal)"
|
|
}
|
|
|
|
case .sweepEnd:
|
|
if collectingRefs {
|
|
if let r = refRtia, r < 8 {
|
|
eisRefs[Int(r)] = eisPoints
|
|
}
|
|
eisPoints.removeAll()
|
|
} else {
|
|
saveEis()
|
|
status = "Sweep complete: \(eisPoints.count) points"
|
|
}
|
|
|
|
case .config(let cfg):
|
|
freqStart = String(format: "%.0f", cfg.freqStart)
|
|
freqStop = String(format: "%.0f", cfg.freqStop)
|
|
ppd = "\(cfg.ppd)"
|
|
rtia = cfg.rtia
|
|
rcal = cfg.rcal
|
|
electrode = cfg.electrode
|
|
status = "Config received"
|
|
|
|
case .lsvStart(let numPoints, let vStart, let vStop):
|
|
lsvPoints.removeAll()
|
|
lsvTotal = numPoints
|
|
status = String(format: "LSV: %d pts, %.0f--%.0f mV", numPoints, vStart, vStop)
|
|
|
|
case .lsvPoint(_, let point):
|
|
lsvPoints.append(point)
|
|
status = "LSV: \(lsvPoints.count)/\(lsvTotal)"
|
|
|
|
case .lsvEnd:
|
|
saveLsv()
|
|
lsvPeaks = detectLsvPeaks(lsvPoints)
|
|
var st = "LSV complete: \(lsvPoints.count) points"
|
|
if let s = phSlope, let o = phOffset, abs(s) > 1e-6 {
|
|
if let peak = detectQhqPeak(lsvPoints) {
|
|
let ph = (Double(peak) - o) / s
|
|
st += String(format: " | pH=%.2f", ph)
|
|
}
|
|
}
|
|
status = st
|
|
|
|
case .ampStart(let vHold):
|
|
ampPoints.removeAll()
|
|
ampRunning = true
|
|
status = String(format: "Amp: %.0f mV", vHold)
|
|
|
|
case .ampPoint(let index, let point):
|
|
ampPoints.append(point)
|
|
ampTotal = index + 1
|
|
status = "Amp: \(ampPoints.count) pts"
|
|
|
|
case .ampEnd:
|
|
ampRunning = false
|
|
saveAmp()
|
|
status = "Amp complete: \(ampPoints.count) points"
|
|
|
|
case .clStart(let numPoints):
|
|
clPoints.removeAll()
|
|
clResult = nil
|
|
clTotal = numPoints
|
|
status = "Chlorine: \(numPoints) pts"
|
|
|
|
case .clPoint(_, let point):
|
|
clPoints.append(point)
|
|
status = "Chlorine: \(clPoints.count)/\(clTotal)"
|
|
|
|
case .clResult(let r):
|
|
clResult = r
|
|
status = String(format: "Chlorine: free=%.3f uA, total=%.3f uA", r.iFreeUa, r.iTotalUa)
|
|
|
|
case .clEnd:
|
|
saveCl()
|
|
status = "Chlorine complete: \(clPoints.count) points"
|
|
|
|
case .phResult(let r):
|
|
if collectingRefs {
|
|
phRef = r
|
|
} else {
|
|
savePh(r)
|
|
status = String(format: "pH: %.2f (OCP=%.1f mV, T=%.1fC)", r.ph, r.vOcpMv, r.tempC)
|
|
phResult = r
|
|
}
|
|
|
|
case .temperature(let t):
|
|
tempC = t
|
|
|
|
case .refFrame(let mode, let rtiaIdx):
|
|
refMode = mode
|
|
refRtia = rtiaIdx
|
|
let modeName: String
|
|
switch mode {
|
|
case 0: modeName = "EIS"
|
|
case 1: modeName = "LSV"
|
|
case 2: modeName = "Amp"
|
|
case 3: modeName = "Cl"
|
|
case 4: modeName = "pH"
|
|
default: modeName = "?"
|
|
}
|
|
if mode == 0 {
|
|
status = "Ref: \(modeName) RTIA \(rtiaIdx + 1)/8"
|
|
} else {
|
|
status = "Ref: \(modeName) range search"
|
|
}
|
|
|
|
case .refLpRange:
|
|
break
|
|
|
|
case .refsDone:
|
|
collectingRefs = false
|
|
hasDeviceRefs = true
|
|
refMode = nil
|
|
refRtia = nil
|
|
let idx = Int(rtia.rawValue)
|
|
if idx < 8, let pts = eisRefs[idx] {
|
|
eisRef = pts
|
|
}
|
|
status = "Reference collection complete"
|
|
|
|
case .refStatus(let hasRefs):
|
|
hasDeviceRefs = hasRefs
|
|
if !hasRefs {
|
|
status = "No device refs"
|
|
}
|
|
|
|
case .cellK(let k):
|
|
calCellConstant = Double(k)
|
|
status = String(format: "Device cell constant: %.4f cm\u{207B}\u{00B9}", k)
|
|
|
|
case .clFactor(let f):
|
|
clFactor = Double(f)
|
|
status = String(format: "Device Cl factor: %.6f", f)
|
|
|
|
case .phCal(let slope, let offset):
|
|
phSlope = Double(slope)
|
|
phOffset = Double(offset)
|
|
status = String(format: "pH cal: slope=%.4f offset=%.4f", slope, offset)
|
|
}
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
func applyEISSettings() {
|
|
let fs = Float(freqStart) ?? 1000
|
|
let fe = Float(freqStop) ?? 200000
|
|
let p = UInt16(ppd) ?? 10
|
|
send(buildSysexSetSweep(freqStart: fs, freqStop: fe, ppd: p))
|
|
send(buildSysexSetRtia(rtia))
|
|
send(buildSysexSetRcal(rcal))
|
|
send(buildSysexSetElectrode(electrode))
|
|
send(buildSysexGetConfig())
|
|
}
|
|
|
|
func startSweep() {
|
|
eisPoints.removeAll()
|
|
send(buildSysexGetTemp())
|
|
send(buildSysexStartSweep())
|
|
}
|
|
|
|
func lsvCalcPoints() -> UInt16 {
|
|
let vs = Float(lsvStartV) ?? 0
|
|
let ve = Float(lsvStopV) ?? 500
|
|
let sr = Float(lsvScanRate) ?? 50
|
|
let d = Float(lsvDensity) ?? 1
|
|
let range = abs(ve - vs)
|
|
let raw: Float
|
|
switch lsvDensityMode {
|
|
case .ptsPerMv:
|
|
raw = range * d
|
|
case .ptsPerSec:
|
|
raw = abs(sr) < 0.001 ? 2 : (range / abs(sr)) * d
|
|
}
|
|
return max(2, min(500, UInt16(raw)))
|
|
}
|
|
|
|
func startLSV() {
|
|
lsvPoints.removeAll()
|
|
let vs = Float(lsvStartV) ?? 0
|
|
let ve = Float(lsvStopV) ?? 500
|
|
let sr = Float(lsvScanRate) ?? 50
|
|
let n = lsvCalcPoints()
|
|
send(buildSysexGetTemp())
|
|
send(buildSysexStartLsv(vStart: vs, vStop: ve, scanRate: sr, lpRtia: lsvRtia, numPoints: n))
|
|
}
|
|
|
|
func startAmp() {
|
|
ampPoints.removeAll()
|
|
ampRunning = true
|
|
let vh = Float(ampVHold) ?? 200
|
|
let iv = Float(ampInterval) ?? 100
|
|
let dur = Float(ampDuration) ?? 60
|
|
send(buildSysexGetTemp())
|
|
send(buildSysexStartAmp(vHold: vh, intervalMs: iv, durationS: dur, lpRtia: ampRtia))
|
|
}
|
|
|
|
func stopAmp() {
|
|
send(buildSysexStopAmp())
|
|
}
|
|
|
|
func startChlorine() {
|
|
clPoints.removeAll()
|
|
clResult = nil
|
|
let vCond = Float(clCondV) ?? 800
|
|
let tCond = Float(clCondT) ?? 2000
|
|
let vFree = Float(clFreeV) ?? 100
|
|
let vTotal = Float(clTotalV) ?? -200
|
|
let tDep = Float(clDepT) ?? 5000
|
|
let tMeas = Float(clMeasT) ?? 5000
|
|
send(buildSysexGetTemp())
|
|
send(buildSysexStartCl(
|
|
vCond: vCond, tCondMs: tCond, vFree: vFree, vTotal: vTotal,
|
|
tDepMs: tDep, tMeasMs: tMeas, lpRtia: clRtia
|
|
))
|
|
}
|
|
|
|
func startPh() {
|
|
phResult = nil
|
|
let stab = Float(phStabilize) ?? 30
|
|
send(buildSysexGetTemp())
|
|
send(buildSysexStartPh(stabilizeS: stab))
|
|
}
|
|
|
|
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 .calibrate, .sessions, .connection: break
|
|
}
|
|
}
|
|
|
|
func collectRefs() {
|
|
collectingRefs = true
|
|
eisRefs.removeAll()
|
|
status = "Starting reference collection..."
|
|
send(buildSysexStartRefs())
|
|
}
|
|
|
|
func getRefs() {
|
|
collectingRefs = true
|
|
eisRefs.removeAll()
|
|
send(buildSysexGetRefs())
|
|
}
|
|
|
|
func clearRefs() {
|
|
collectingRefs = false
|
|
hasDeviceRefs = false
|
|
eisRefs.removeAll()
|
|
eisRef = nil
|
|
phRef = nil
|
|
send(buildSysexClearRefs())
|
|
status = "Refs cleared"
|
|
}
|
|
|
|
func startClean() {
|
|
let v = Float(cleanV) ?? 1200
|
|
let d = Float(cleanDur) ?? 30
|
|
send(buildSysexStartClean(vMv: v, durationS: d))
|
|
status = String(format: "Cleaning: %.0f mV for %.0fs", v, d)
|
|
}
|
|
|
|
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 .calibrate, .sessions, .connection: 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 .calibrate, .sessions, .connection: false
|
|
}
|
|
}
|
|
|
|
// MARK: - Auto-save
|
|
|
|
private func saveEis() {
|
|
guard let sid = currentSessionId else { return }
|
|
let params: [String: String] = [
|
|
"freq_start": freqStart,
|
|
"freq_stop": freqStop,
|
|
"ppd": ppd,
|
|
"rtia": rtia.label,
|
|
"rcal": rcal.label,
|
|
"electrode": electrode.label,
|
|
]
|
|
guard let configData = try? JSONEncoder().encode(params) else { return }
|
|
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .eis) else { return }
|
|
meas.config = configData
|
|
guard let mid = meas.id else { return }
|
|
let indexed = eisPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
|
try? Storage.shared.addDataPoints(measurementId: mid, points: indexed)
|
|
}
|
|
|
|
private func saveLsv() {
|
|
guard let sid = currentSessionId else { return }
|
|
let params: [String: String] = [
|
|
"v_start": lsvStartV,
|
|
"v_stop": lsvStopV,
|
|
"scan_rate": lsvScanRate,
|
|
"rtia": lsvRtia.label,
|
|
]
|
|
guard let configData = try? JSONEncoder().encode(params) else { return }
|
|
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .lsv) else { return }
|
|
meas.config = configData
|
|
guard let mid = meas.id else { return }
|
|
let indexed = lsvPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
|
try? Storage.shared.addDataPoints(measurementId: mid, points: indexed)
|
|
}
|
|
|
|
private func saveAmp() {
|
|
guard let sid = currentSessionId else { return }
|
|
let params: [String: String] = [
|
|
"v_hold": ampVHold,
|
|
"interval_ms": ampInterval,
|
|
"duration_s": ampDuration,
|
|
"rtia": ampRtia.label,
|
|
]
|
|
guard let configData = try? JSONEncoder().encode(params) else { return }
|
|
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .amp) else { return }
|
|
meas.config = configData
|
|
guard let mid = meas.id else { return }
|
|
let indexed = ampPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
|
try? Storage.shared.addDataPoints(measurementId: mid, points: indexed)
|
|
}
|
|
|
|
private func saveCl() {
|
|
guard let sid = currentSessionId else { return }
|
|
let params: [String: String] = [
|
|
"cond_v": clCondV,
|
|
"cond_t": clCondT,
|
|
"free_v": clFreeV,
|
|
"total_v": clTotalV,
|
|
"dep_t": clDepT,
|
|
"meas_t": clMeasT,
|
|
"rtia": clRtia.label,
|
|
]
|
|
guard let configData = try? JSONEncoder().encode(params) else { return }
|
|
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .chlorine) else { return }
|
|
meas.config = configData
|
|
guard let mid = meas.id else { return }
|
|
let indexed = clPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
|
try? Storage.shared.addDataPoints(measurementId: mid, points: indexed)
|
|
if let r = clResult {
|
|
try? Storage.shared.setMeasurementResult(mid, result: r)
|
|
}
|
|
}
|
|
|
|
private func savePh(_ result: PhResult) {
|
|
guard let sid = currentSessionId else { return }
|
|
let params: [String: String] = [
|
|
"stabilize_s": phStabilize,
|
|
]
|
|
guard let configData = try? JSONEncoder().encode(params) else { return }
|
|
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .ph) else { return }
|
|
meas.config = configData
|
|
guard let mid = meas.id else { return }
|
|
try? Storage.shared.addDataPoint(measurementId: mid, index: 0, point: result)
|
|
try? Storage.shared.setMeasurementResult(mid, result: result)
|
|
}
|
|
|
|
// MARK: - Measurement loading
|
|
|
|
func loadMeasurement(_ measurement: Measurement) {
|
|
guard let id = measurement.id,
|
|
let type = MeasurementType(rawValue: measurement.type) else { return }
|
|
do {
|
|
switch type {
|
|
case .eis:
|
|
eisPoints = try Storage.shared.fetchTypedPoints(measurementId: id, as: EisPoint.self)
|
|
tab = .eis
|
|
status = "Loaded EIS (\(eisPoints.count) pts)"
|
|
case .lsv:
|
|
lsvPoints = try Storage.shared.fetchTypedPoints(measurementId: id, as: LsvPoint.self)
|
|
tab = .lsv
|
|
status = "Loaded LSV (\(lsvPoints.count) pts)"
|
|
case .amp:
|
|
ampPoints = try Storage.shared.fetchTypedPoints(measurementId: id, as: AmpPoint.self)
|
|
tab = .amp
|
|
status = "Loaded Amp (\(ampPoints.count) pts)"
|
|
case .chlorine:
|
|
clPoints = try Storage.shared.fetchTypedPoints(measurementId: id, as: ClPoint.self)
|
|
if let summary = measurement.resultSummary {
|
|
clResult = try JSONDecoder().decode(ClResult.self, from: summary)
|
|
}
|
|
tab = .chlorine
|
|
status = "Loaded Chlorine (\(clPoints.count) pts)"
|
|
case .ph:
|
|
if let summary = measurement.resultSummary {
|
|
phResult = try JSONDecoder().decode(PhResult.self, from: summary)
|
|
}
|
|
tab = .ph
|
|
status = "Loaded pH result"
|
|
}
|
|
} catch {
|
|
status = "Load failed: \(error.localizedDescription)"
|
|
}
|
|
}
|
|
|
|
func loadAsReference(_ measurement: Measurement) {
|
|
guard let id = measurement.id,
|
|
let type = MeasurementType(rawValue: measurement.type) else { return }
|
|
do {
|
|
switch type {
|
|
case .eis:
|
|
eisRef = try Storage.shared.fetchTypedPoints(measurementId: id, as: EisPoint.self)
|
|
status = "EIS reference loaded (\(eisRef?.count ?? 0) pts)"
|
|
case .lsv:
|
|
lsvRef = try Storage.shared.fetchTypedPoints(measurementId: id, as: LsvPoint.self)
|
|
status = "LSV reference loaded (\(lsvRef?.count ?? 0) pts)"
|
|
case .amp:
|
|
ampRef = try Storage.shared.fetchTypedPoints(measurementId: id, as: AmpPoint.self)
|
|
status = "Amp reference loaded (\(ampRef?.count ?? 0) pts)"
|
|
case .chlorine:
|
|
let pts = try Storage.shared.fetchTypedPoints(measurementId: id, as: ClPoint.self)
|
|
if let summary = measurement.resultSummary {
|
|
let result = try JSONDecoder().decode(ClResult.self, from: summary)
|
|
clRef = (pts, result)
|
|
status = "Chlorine reference loaded"
|
|
}
|
|
case .ph:
|
|
if let summary = measurement.resultSummary {
|
|
phRef = try JSONDecoder().decode(PhResult.self, from: summary)
|
|
status = String(format: "pH reference loaded (%.2f)", phRef?.ph ?? 0)
|
|
}
|
|
}
|
|
} catch {
|
|
status = "Reference load failed: \(error.localizedDescription)"
|
|
}
|
|
}
|
|
}
|