iOS: add pH cal protocol, Q/HQ peak detection, and state
This commit is contained in:
parent
d5e1a7dd0f
commit
818c4ff7a2
|
|
@ -97,6 +97,10 @@ final class AppState {
|
|||
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"
|
||||
|
|
@ -262,6 +266,11 @@ final class AppState {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
if points.count < 5 { return [] }
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ let RSP_REF_LP_RANGE: UInt8 = 0x21
|
|||
let RSP_REFS_DONE: UInt8 = 0x22
|
||||
let RSP_REF_STATUS: UInt8 = 0x23
|
||||
let RSP_CL_FACTOR: UInt8 = 0x24
|
||||
let RSP_PH_CAL: UInt8 = 0x25
|
||||
|
||||
// Cue -> ESP32
|
||||
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_SET_CL_FACTOR: UInt8 = 0x33
|
||||
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_GET_REFS: UInt8 = 0x31
|
||||
let CMD_CLEAR_REFS: UInt8 = 0x32
|
||||
|
|
@ -127,6 +130,7 @@ enum EisMessage {
|
|||
case refStatus(hasRefs: Bool)
|
||||
case cellK(Float)
|
||||
case clFactor(Float)
|
||||
case phCal(slope: Float, offset: Float)
|
||||
}
|
||||
|
||||
// MARK: - Response parser
|
||||
|
|
@ -261,6 +265,12 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
|
|||
case RSP_CL_FACTOR where p.count >= 5:
|
||||
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:
|
||||
return nil
|
||||
}
|
||||
|
|
@ -389,3 +399,15 @@ func buildSysexSetClFactor(_ f: Float) -> [UInt8] {
|
|||
func buildSysexGetClFactor() -> [UInt8] {
|
||||
[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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ final class UDPManager: @unchecked Sendable {
|
|||
send(buildSysexGetConfig())
|
||||
send(buildSysexGetCellK())
|
||||
send(buildSysexGetClFactor())
|
||||
send(buildSysexGetPhCal())
|
||||
startTimers()
|
||||
receiveLoop()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue