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 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 [] }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue