iOS: add pH cal protocol, Q/HQ peak detection, and state

This commit is contained in:
jess 2026-04-02 19:34:02 -07:00
parent d5e1a7dd0f
commit 818c4ff7a2
4 changed files with 58 additions and 0 deletions

View File

@ -97,6 +97,10 @@ final class AppState {
var calRs: Double? = nil var calRs: Double? = nil
var clFactor: Double? = nil var clFactor: Double? = nil
var clCalKnownPpm: String = "5" var clCalKnownPpm: String = "5"
var phSlope: Double? = nil
var phOffset: Double? = nil
var phCalPoints: [(ph: Double, mV: Double)] = []
var phCalKnown: String = "7.00"
// Clean // Clean
var cleanV: String = "1200" var cleanV: String = "1200"
@ -262,6 +266,11 @@ final class AppState {
case .clFactor(let f): case .clFactor(let f):
clFactor = Double(f) clFactor = Double(f)
status = String(format: "Device Cl factor: %.6f", 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)
} }
} }

View File

@ -59,6 +59,32 @@ func findExtrema(_ v: [Float], _ iSmooth: [Float], minProminence: Float) -> [(In
} }
} }
/// Detect Q/HQ redox peak in the -100 to +600 mV window.
/// Returns peak voltage in mV if found.
func detectQhqPeak(_ points: [LsvPoint]) -> Float? {
if points.count < 5 { return nil }
let iVals = points.map { $0.iUa }
let vVals = points.map { $0.vMv }
let window = max(5, points.count / 50)
let smoothed = smoothLsv(iVals, window: window)
guard let iMin = smoothed.min(), let iMax = smoothed.max() else { return nil }
let prominence = (iMax - iMin) * 0.05
let extrema = findExtrema(vVals, smoothed, minProminence: prominence)
let candidates = extrema
.filter { $0.1 && vVals[$0.0] >= -100 && vVals[$0.0] <= 600 }
.max(by: { smoothed[$0.0] < smoothed[$1.0] })
if let (idx, _) = candidates {
return vVals[idx]
}
return nil
}
func detectLsvPeaks(_ points: [LsvPoint]) -> [LsvPeak] { func detectLsvPeaks(_ points: [LsvPoint]) -> [LsvPeak] {
if points.count < 5 { return [] } if points.count < 5 { return [] }

View File

@ -30,6 +30,7 @@ let RSP_REF_LP_RANGE: UInt8 = 0x21
let RSP_REFS_DONE: UInt8 = 0x22 let RSP_REFS_DONE: UInt8 = 0x22
let RSP_REF_STATUS: UInt8 = 0x23 let RSP_REF_STATUS: UInt8 = 0x23
let RSP_CL_FACTOR: UInt8 = 0x24 let RSP_CL_FACTOR: UInt8 = 0x24
let RSP_PH_CAL: UInt8 = 0x25
// Cue -> ESP32 // Cue -> ESP32
let CMD_SET_SWEEP: UInt8 = 0x10 let CMD_SET_SWEEP: UInt8 = 0x10
@ -49,6 +50,8 @@ let CMD_SET_CELL_K: UInt8 = 0x28
let CMD_GET_CELL_K: UInt8 = 0x29 let CMD_GET_CELL_K: UInt8 = 0x29
let CMD_SET_CL_FACTOR: UInt8 = 0x33 let CMD_SET_CL_FACTOR: UInt8 = 0x33
let CMD_GET_CL_FACTOR: UInt8 = 0x34 let CMD_GET_CL_FACTOR: UInt8 = 0x34
let CMD_SET_PH_CAL: UInt8 = 0x35
let CMD_GET_PH_CAL: UInt8 = 0x36
let CMD_START_REFS: UInt8 = 0x30 let CMD_START_REFS: UInt8 = 0x30
let CMD_GET_REFS: UInt8 = 0x31 let CMD_GET_REFS: UInt8 = 0x31
let CMD_CLEAR_REFS: UInt8 = 0x32 let CMD_CLEAR_REFS: UInt8 = 0x32
@ -127,6 +130,7 @@ enum EisMessage {
case refStatus(hasRefs: Bool) case refStatus(hasRefs: Bool)
case cellK(Float) case cellK(Float)
case clFactor(Float) case clFactor(Float)
case phCal(slope: Float, offset: Float)
} }
// MARK: - Response parser // MARK: - Response parser
@ -261,6 +265,12 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
case RSP_CL_FACTOR where p.count >= 5: case RSP_CL_FACTOR where p.count >= 5:
return .clFactor(decodeFloat(p, at: 0)) return .clFactor(decodeFloat(p, at: 0))
case RSP_PH_CAL where p.count >= 10:
return .phCal(
slope: decodeFloat(p, at: 0),
offset: decodeFloat(p, at: 5)
)
default: default:
return nil return nil
} }
@ -389,3 +399,15 @@ func buildSysexSetClFactor(_ f: Float) -> [UInt8] {
func buildSysexGetClFactor() -> [UInt8] { func buildSysexGetClFactor() -> [UInt8] {
[0xF0, sysexMfr, CMD_GET_CL_FACTOR, 0xF7] [0xF0, sysexMfr, CMD_GET_CL_FACTOR, 0xF7]
} }
func buildSysexSetPhCal(_ slope: Float, _ offset: Float) -> [UInt8] {
var sx: [UInt8] = [0xF0, sysexMfr, CMD_SET_PH_CAL]
sx.append(contentsOf: encodeFloat(slope))
sx.append(contentsOf: encodeFloat(offset))
sx.append(0xF7)
return sx
}
func buildSysexGetPhCal() -> [UInt8] {
[0xF0, sysexMfr, CMD_GET_PH_CAL, 0xF7]
}

View File

@ -96,6 +96,7 @@ final class UDPManager: @unchecked Sendable {
send(buildSysexGetConfig()) send(buildSysexGetConfig())
send(buildSysexGetCellK()) send(buildSysexGetCellK())
send(buildSysexGetClFactor()) send(buildSysexGetClFactor())
send(buildSysexGetPhCal())
startTimers() startTimers()
receiveLoop() receiveLoop()