longer settling time for lower freqs
This commit is contained in:
parent
332aeb10d6
commit
c98445b377
|
|
@ -17,6 +17,7 @@ enum Tab: String, CaseIterable, Identifiable {
|
||||||
case amp = "Amperometry"
|
case amp = "Amperometry"
|
||||||
case chlorine = "Chlorine"
|
case chlorine = "Chlorine"
|
||||||
case ph = "pH"
|
case ph = "pH"
|
||||||
|
case orp = "ORP"
|
||||||
case calibrate = "Calibrate"
|
case calibrate = "Calibrate"
|
||||||
case sessions = "Sessions"
|
case sessions = "Sessions"
|
||||||
case connection = "Connection"
|
case connection = "Connection"
|
||||||
|
|
@ -84,12 +85,19 @@ final class AppState {
|
||||||
var phResult: PhResult? = nil
|
var phResult: PhResult? = nil
|
||||||
var phStabilize: String = "30"
|
var phStabilize: String = "30"
|
||||||
|
|
||||||
|
// ORP
|
||||||
|
var orpResult: OrpResult? = nil
|
||||||
|
var orpStabilize: String = "30"
|
||||||
|
var orpHistory: [OrpSample] = []
|
||||||
|
private var orpT0: Date? = nil
|
||||||
|
|
||||||
// Reference baselines
|
// Reference baselines
|
||||||
var eisRef: [EisPoint]? = nil
|
var eisRef: [EisPoint]? = nil
|
||||||
var lsvRef: [LsvPoint]? = nil
|
var lsvRef: [LsvPoint]? = nil
|
||||||
var ampRef: [AmpPoint]? = nil
|
var ampRef: [AmpPoint]? = nil
|
||||||
var clRef: (points: [ClPoint], result: ClResult)? = nil
|
var clRef: (points: [ClPoint], result: ClResult)? = nil
|
||||||
var phRef: PhResult? = nil
|
var phRef: PhResult? = nil
|
||||||
|
var orpRef: OrpResult? = nil
|
||||||
|
|
||||||
// Device reference collection
|
// Device reference collection
|
||||||
var collectingRefs: Bool = false
|
var collectingRefs: Bool = false
|
||||||
|
|
@ -291,6 +299,23 @@ final class AppState {
|
||||||
phResult = r
|
phResult = r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case .orpResult(let r):
|
||||||
|
transport.measuring = false
|
||||||
|
if collectingRefs {
|
||||||
|
orpRef = r
|
||||||
|
} else {
|
||||||
|
saveOrp(r)
|
||||||
|
let t0 = orpT0 ?? {
|
||||||
|
let now = Date()
|
||||||
|
orpT0 = now
|
||||||
|
return now
|
||||||
|
}()
|
||||||
|
let tS = Float(Date().timeIntervalSince(t0))
|
||||||
|
orpHistory.append(OrpSample(tS: tS, vMv: r.vOrpMv))
|
||||||
|
status = String(format: "ORP: %.0f mV (T=%.1fC)", r.vOrpMv, r.tempC)
|
||||||
|
orpResult = r
|
||||||
|
}
|
||||||
|
|
||||||
case .temperature(let t):
|
case .temperature(let t):
|
||||||
tempC = t
|
tempC = t
|
||||||
|
|
||||||
|
|
@ -545,6 +570,20 @@ final class AppState {
|
||||||
send(buildSysexStartPh(stabilizeS: stab))
|
send(buildSysexStartPh(stabilizeS: stab))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startOrp() {
|
||||||
|
orpResult = nil
|
||||||
|
transport.measuring = true
|
||||||
|
let stab = Float(orpStabilize) ?? 30
|
||||||
|
send(buildSysexGetTemp())
|
||||||
|
send(buildSysexStartOrp(stabilizeS: stab))
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearOrpHistory() {
|
||||||
|
orpHistory.removeAll()
|
||||||
|
orpT0 = nil
|
||||||
|
status = "ORP history cleared"
|
||||||
|
}
|
||||||
|
|
||||||
func phCalStartMeasurement() {
|
func phCalStartMeasurement() {
|
||||||
guard let stabilize = Float(phCalStabilize) else { return }
|
guard let stabilize = Float(phCalStabilize) else { return }
|
||||||
send(buildSysexPhCalPoint(bufferId: UInt8(phCalSelectedBuf), tempSlot: UInt8(phCalSelectedTslot), stabilizeS: stabilize))
|
send(buildSysexPhCalPoint(bufferId: UInt8(phCalSelectedBuf), tempSlot: UInt8(phCalSelectedTslot), stabilizeS: stabilize))
|
||||||
|
|
@ -583,6 +622,11 @@ final class AppState {
|
||||||
phRef = r
|
phRef = r
|
||||||
status = String(format: "pH reference set (%.2f)", r.ph)
|
status = String(format: "pH reference set (%.2f)", r.ph)
|
||||||
}
|
}
|
||||||
|
case .orp:
|
||||||
|
if let r = orpResult {
|
||||||
|
orpRef = r
|
||||||
|
status = String(format: "ORP reference set (%.0f mV)", r.vOrpMv)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -595,6 +639,7 @@ final class AppState {
|
||||||
case .amp: ampRef = nil; status = "Amp reference cleared"
|
case .amp: ampRef = nil; status = "Amp reference cleared"
|
||||||
case .chlorine: clRef = nil; status = "Chlorine reference cleared"
|
case .chlorine: clRef = nil; status = "Chlorine reference cleared"
|
||||||
case .ph: phRef = nil; status = "pH reference cleared"
|
case .ph: phRef = nil; status = "pH reference cleared"
|
||||||
|
case .orp: orpRef = nil; status = "ORP reference cleared"
|
||||||
case .calibrate, .sessions, .connection: break
|
case .calibrate, .sessions, .connection: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -643,6 +688,7 @@ final class AppState {
|
||||||
case .amp: ampRef != nil
|
case .amp: ampRef != nil
|
||||||
case .chlorine: clRef != nil
|
case .chlorine: clRef != nil
|
||||||
case .ph: phRef != nil
|
case .ph: phRef != nil
|
||||||
|
case .orp: orpRef != nil
|
||||||
case .calibrate, .sessions, .connection: false
|
case .calibrate, .sessions, .connection: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -654,6 +700,7 @@ final class AppState {
|
||||||
case .amp: !ampPoints.isEmpty
|
case .amp: !ampPoints.isEmpty
|
||||||
case .chlorine: clResult != nil
|
case .chlorine: clResult != nil
|
||||||
case .ph: phResult != nil
|
case .ph: phResult != nil
|
||||||
|
case .orp: orpResult != nil
|
||||||
case .calibrate, .sessions, .connection: false
|
case .calibrate, .sessions, .connection: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -755,6 +802,21 @@ final class AppState {
|
||||||
try? Storage.shared.setMeasurementResult(mid, result: result)
|
try? Storage.shared.setMeasurementResult(mid, result: result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func saveOrp(_ result: OrpResult) {
|
||||||
|
guard let sid = currentSessionId else { return }
|
||||||
|
let ts = pendingEspTimestamp
|
||||||
|
pendingEspTimestamp = nil
|
||||||
|
let params: [String: String] = [
|
||||||
|
"stabilize_s": orpStabilize,
|
||||||
|
]
|
||||||
|
guard let configData = try? JSONEncoder().encode(params) else { return }
|
||||||
|
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .orp, espTimestamp: ts) 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
|
// MARK: - Measurement loading
|
||||||
|
|
||||||
func loadMeasurement(_ measurement: Measurement) {
|
func loadMeasurement(_ measurement: Measurement) {
|
||||||
|
|
@ -787,6 +849,12 @@ final class AppState {
|
||||||
}
|
}
|
||||||
tab = .ph
|
tab = .ph
|
||||||
status = "Loaded pH result"
|
status = "Loaded pH result"
|
||||||
|
case .orp:
|
||||||
|
if let summary = measurement.resultSummary {
|
||||||
|
orpResult = try JSONDecoder().decode(OrpResult.self, from: summary)
|
||||||
|
}
|
||||||
|
tab = .orp
|
||||||
|
status = "Loaded ORP result"
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
status = "Load failed: \(error.localizedDescription)"
|
status = "Load failed: \(error.localizedDescription)"
|
||||||
|
|
@ -819,6 +887,11 @@ final class AppState {
|
||||||
phRef = try JSONDecoder().decode(PhResult.self, from: summary)
|
phRef = try JSONDecoder().decode(PhResult.self, from: summary)
|
||||||
status = String(format: "pH reference loaded (%.2f)", phRef?.ph ?? 0)
|
status = String(format: "pH reference loaded (%.2f)", phRef?.ph ?? 0)
|
||||||
}
|
}
|
||||||
|
case .orp:
|
||||||
|
if let summary = measurement.resultSummary {
|
||||||
|
orpRef = try JSONDecoder().decode(OrpResult.self, from: summary)
|
||||||
|
status = String(format: "ORP reference loaded (%.0f mV)", orpRef?.vOrpMv ?? 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
status = "Reference load failed: \(error.localizedDescription)"
|
status = "Reference load failed: \(error.localizedDescription)"
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 179 KiB |
|
|
@ -66,6 +66,17 @@ struct PhResult: Codable {
|
||||||
var tempC: Float
|
var tempC: Float
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct OrpResult: Codable {
|
||||||
|
var vOrpMv: Float
|
||||||
|
var tempC: Float
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OrpSample: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let tS: Float
|
||||||
|
let vMv: Float
|
||||||
|
}
|
||||||
|
|
||||||
struct PhCalCell {
|
struct PhCalCell {
|
||||||
let ocpMv: Float
|
let ocpMv: Float
|
||||||
let tempC: Float
|
let tempC: Float
|
||||||
|
|
@ -210,4 +221,5 @@ enum MeasurementType: String, Codable {
|
||||||
case amp
|
case amp
|
||||||
case chlorine
|
case chlorine
|
||||||
case ph
|
case ph
|
||||||
|
case orp
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ let RSP_CL_END: UInt8 = 0x0E
|
||||||
let RSP_PH_RESULT: UInt8 = 0x0F
|
let RSP_PH_RESULT: UInt8 = 0x0F
|
||||||
let RSP_TEMP: UInt8 = 0x10
|
let RSP_TEMP: UInt8 = 0x10
|
||||||
let RSP_CELL_K: UInt8 = 0x11
|
let RSP_CELL_K: UInt8 = 0x11
|
||||||
|
let RSP_ORP_RESULT: UInt8 = 0x12
|
||||||
let RSP_REF_FRAME: UInt8 = 0x20
|
let RSP_REF_FRAME: UInt8 = 0x20
|
||||||
let RSP_REF_LP_RANGE: UInt8 = 0x21
|
let RSP_REF_LP_RANGE: UInt8 = 0x21
|
||||||
let RSP_REFS_DONE: UInt8 = 0x22
|
let RSP_REFS_DONE: UInt8 = 0x22
|
||||||
|
|
@ -53,6 +54,7 @@ let CMD_STOP_AMP: UInt8 = 0x22
|
||||||
let CMD_START_CL: UInt8 = 0x23
|
let CMD_START_CL: UInt8 = 0x23
|
||||||
let CMD_START_PH: UInt8 = 0x24
|
let CMD_START_PH: UInt8 = 0x24
|
||||||
let CMD_START_CLEAN: UInt8 = 0x25
|
let CMD_START_CLEAN: UInt8 = 0x25
|
||||||
|
let CMD_START_ORP: UInt8 = 0x2A
|
||||||
let CMD_SET_CELL_K: UInt8 = 0x28
|
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
|
||||||
|
|
@ -161,6 +163,7 @@ enum EisMessage {
|
||||||
case clResult(ClResult)
|
case clResult(ClResult)
|
||||||
case clEnd
|
case clEnd
|
||||||
case phResult(PhResult)
|
case phResult(PhResult)
|
||||||
|
case orpResult(OrpResult)
|
||||||
case temperature(Float)
|
case temperature(Float)
|
||||||
case refFrame(mode: UInt8, rtiaIdx: UInt8)
|
case refFrame(mode: UInt8, rtiaIdx: UInt8)
|
||||||
case refLpRange(mode: UInt8, lowIdx: UInt8, highIdx: UInt8)
|
case refLpRange(mode: UInt8, lowIdx: UInt8, highIdx: UInt8)
|
||||||
|
|
@ -305,6 +308,12 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
|
||||||
tempC: decodeFloat(p, at: 10)
|
tempC: decodeFloat(p, at: 10)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
case RSP_ORP_RESULT where p.count >= 10:
|
||||||
|
return .orpResult(OrpResult(
|
||||||
|
vOrpMv: decodeFloat(p, at: 0),
|
||||||
|
tempC: decodeFloat(p, at: 5)
|
||||||
|
))
|
||||||
|
|
||||||
case RSP_TEMP where p.count >= 5:
|
case RSP_TEMP where p.count >= 5:
|
||||||
return .temperature(decodeFloat(p, at: 0))
|
return .temperature(decodeFloat(p, at: 0))
|
||||||
|
|
||||||
|
|
@ -482,6 +491,13 @@ func buildSysexStartPh(stabilizeS: Float) -> [UInt8] {
|
||||||
return sx
|
return sx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildSysexStartOrp(stabilizeS: Float) -> [UInt8] {
|
||||||
|
var sx: [UInt8] = [0xF0, sysexMfr, CMD_START_ORP]
|
||||||
|
sx.append(contentsOf: encodeFloat(stabilizeS))
|
||||||
|
sx.append(0xF7)
|
||||||
|
return sx
|
||||||
|
}
|
||||||
|
|
||||||
func buildSysexStartClean(vMv: Float, durationS: Float) -> [UInt8] {
|
func buildSysexStartClean(vMv: Float, durationS: Float) -> [UInt8] {
|
||||||
var sx: [UInt8] = [0xF0, sysexMfr, CMD_START_CLEAN]
|
var sx: [UInt8] = [0xF0, sysexMfr, CMD_START_CLEAN]
|
||||||
sx.append(contentsOf: encodeFloat(vMv))
|
sx.append(contentsOf: encodeFloat(vMv))
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,14 @@ final class Storage: @unchecked Sendable {
|
||||||
out += "\"Temperature (C)\" = \(tomlFloat(p.tempC))\n"
|
out += "\"Temperature (C)\" = \(tomlFloat(p.tempC))\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case .orp:
|
||||||
|
for dp in points {
|
||||||
|
if let p = try? decoder.decode(OrpResult.self, from: dp.payload) {
|
||||||
|
out += "\n[[measurement.data]]\n"
|
||||||
|
out += "\"ORP (mV)\" = \(tomlFloat(p.vOrpMv))\n"
|
||||||
|
out += "\"Temperature (C)\" = \(tomlFloat(p.tempC))\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
case nil:
|
case nil:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -478,6 +486,11 @@ final class Storage: @unchecked Sendable {
|
||||||
ph: floatVal(row, "pH"),
|
ph: floatVal(row, "pH"),
|
||||||
tempC: floatVal(row, "Temperature (C)")
|
tempC: floatVal(row, "Temperature (C)")
|
||||||
))
|
))
|
||||||
|
case .orp:
|
||||||
|
payload = try encoder.encode(OrpResult(
|
||||||
|
vOrpMv: floatVal(row, "ORP (mV)"),
|
||||||
|
tempC: floatVal(row, "Temperature (C)")
|
||||||
|
))
|
||||||
case nil:
|
case nil:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -501,6 +514,13 @@ final class Storage: @unchecked Sendable {
|
||||||
)
|
)
|
||||||
try setMeasurementResult(mid, result: r)
|
try setMeasurementResult(mid, result: r)
|
||||||
}
|
}
|
||||||
|
if mtype == .orp, let dataRows = mDict["data"] as? [[String: Any]], let first = dataRows.first {
|
||||||
|
let r = OrpResult(
|
||||||
|
vOrpMv: floatVal(first, "ORP (mV)"),
|
||||||
|
tempC: floatVal(first, "Temperature (C)")
|
||||||
|
)
|
||||||
|
try setMeasurementResult(mid, result: r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return sid
|
return sid
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ struct ContentView: View {
|
||||||
sidebarButton(.amp, "Amperometry", "bolt.fill")
|
sidebarButton(.amp, "Amperometry", "bolt.fill")
|
||||||
sidebarButton(.chlorine, "Chlorine", "drop.fill")
|
sidebarButton(.chlorine, "Chlorine", "drop.fill")
|
||||||
sidebarButton(.ph, "pH", "scalemass")
|
sidebarButton(.ph, "pH", "scalemass")
|
||||||
|
sidebarButton(.orp, "ORP", "bolt.circle.fill")
|
||||||
}
|
}
|
||||||
Section("Tools") {
|
Section("Tools") {
|
||||||
sidebarButton(.calibrate, "Calibrate", "lines.measurement.horizontal")
|
sidebarButton(.calibrate, "Calibrate", "lines.measurement.horizontal")
|
||||||
|
|
@ -127,6 +128,13 @@ struct ContentView: View {
|
||||||
.tabItem { Label("pH", systemImage: "scalemass") }
|
.tabItem { Label("pH", systemImage: "scalemass") }
|
||||||
.tag(Tab.ph)
|
.tag(Tab.ph)
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
StatusBar(state: state)
|
||||||
|
OrpView(state: state)
|
||||||
|
}
|
||||||
|
.tabItem { Label("ORP", systemImage: "bolt.circle.fill") }
|
||||||
|
.tag(Tab.orp)
|
||||||
|
|
||||||
CalibrateView(state: state)
|
CalibrateView(state: state)
|
||||||
.tabItem { Label("Calibrate", systemImage: "lines.measurement.horizontal") }
|
.tabItem { Label("Calibrate", systemImage: "lines.measurement.horizontal") }
|
||||||
.tag(Tab.calibrate)
|
.tag(Tab.calibrate)
|
||||||
|
|
@ -151,6 +159,7 @@ struct ContentView: View {
|
||||||
case .amp: AmpView(state: state)
|
case .amp: AmpView(state: state)
|
||||||
case .chlorine: ChlorineView(state: state)
|
case .chlorine: ChlorineView(state: state)
|
||||||
case .ph: PhView(state: state)
|
case .ph: PhView(state: state)
|
||||||
|
case .orp: OrpView(state: state)
|
||||||
case .calibrate: CalibrateView(state: state)
|
case .calibrate: CalibrateView(state: state)
|
||||||
case .sessions: SessionView(state: state)
|
case .sessions: SessionView(state: state)
|
||||||
case .connection: ConnectionView(state: state)
|
case .connection: ConnectionView(state: state)
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ struct MeasurementDataView: View {
|
||||||
)
|
)
|
||||||
case .ph:
|
case .ph:
|
||||||
PhDataView(result: decodeResult(PhResult.self))
|
PhDataView(result: decodeResult(PhResult.self))
|
||||||
|
case .orp:
|
||||||
|
OrpDataView(result: decodeResult(OrpResult.self))
|
||||||
case nil:
|
case nil:
|
||||||
Text("Unknown type: \(measurement.type)")
|
Text("Unknown type: \(measurement.type)")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
@ -58,6 +60,7 @@ struct MeasurementDataView: View {
|
||||||
case "amp": "Amperometry"
|
case "amp": "Amperometry"
|
||||||
case "chlorine": "Chlorine"
|
case "chlorine": "Chlorine"
|
||||||
case "ph": "pH"
|
case "ph": "pH"
|
||||||
|
case "orp": "ORP"
|
||||||
default: measurement.type
|
default: measurement.type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -639,3 +642,29 @@ struct PhDataView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct OrpDataView: View {
|
||||||
|
let result: OrpResult?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
if let r = result {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text(String(format: "ORP: %.0f mV", r.vOrpMv))
|
||||||
|
.font(.system(size: 56, weight: .bold, design: .rounded))
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
|
Text(String(format: "Temperature: %.1f C", r.tempC))
|
||||||
|
.font(.title3)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||||
|
.padding()
|
||||||
|
} else {
|
||||||
|
Text("No ORP result")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Charts
|
||||||
|
|
||||||
|
struct OrpView: View {
|
||||||
|
@Bindable var state: AppState
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
controlsRow
|
||||||
|
Divider()
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
headerValues
|
||||||
|
chart
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Controls
|
||||||
|
|
||||||
|
private var controlsRow: some View {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
LabeledField("Stabilize s", text: $state.orpStabilize, width: 80)
|
||||||
|
|
||||||
|
Button("Measure ORP") { state.startOrp() }
|
||||||
|
.buttonStyle(ActionButtonStyle(color: .green))
|
||||||
|
|
||||||
|
Button("Clear") { state.clearOrpHistory() }
|
||||||
|
.buttonStyle(ActionButtonStyle(color: Color(red: 0.55, green: 0.3, blue: 0.3)))
|
||||||
|
|
||||||
|
Text("n=\(state.orpHistory.count)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Header
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var headerValues: some View {
|
||||||
|
if let r = state.orpResult {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text(String(format: "ORP: %.0f mV", r.vOrpMv))
|
||||||
|
.font(.system(size: 40, weight: .bold, design: .rounded))
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
|
Text(String(format: "Temp: %.1f\u{00B0}C", r.tempC))
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
if let refR = state.orpRef {
|
||||||
|
let dV = r.vOrpMv - refR.vOrpMv
|
||||||
|
Text(String(format: "vs Ref: dORP=%+.1f mV (ref=%.0f mV)",
|
||||||
|
dV, refR.vOrpMv))
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text("No measurement yet")
|
||||||
|
.font(.title3)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text("Open-circuit potential vs AgCl reference. Above ~650 mV indicates sanitary water.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color(white: 0.4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Chart
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var chart: some View {
|
||||||
|
if state.orpHistory.isEmpty {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color.white.opacity(0.03))
|
||||||
|
.overlay(
|
||||||
|
Text("No samples yet")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Chart(state.orpHistory) { sample in
|
||||||
|
LineMark(
|
||||||
|
x: .value("t", Double(sample.tS)),
|
||||||
|
y: .value("mV", Double(sample.vMv))
|
||||||
|
)
|
||||||
|
.foregroundStyle(Color.orange)
|
||||||
|
.lineStyle(StrokeStyle(lineWidth: 2))
|
||||||
|
|
||||||
|
PointMark(
|
||||||
|
x: .value("t", Double(sample.tS)),
|
||||||
|
y: .value("mV", Double(sample.vMv))
|
||||||
|
)
|
||||||
|
.foregroundStyle(Color.orange)
|
||||||
|
.symbolSize(30)
|
||||||
|
}
|
||||||
|
.chartXAxisLabel("t (s)")
|
||||||
|
.chartYAxisLabel("ORP (mV)")
|
||||||
|
.chartXAxis {
|
||||||
|
AxisMarks { _ in
|
||||||
|
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
|
||||||
|
.foregroundStyle(Color.gray.opacity(0.3))
|
||||||
|
AxisValueLabel()
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chartYAxis {
|
||||||
|
AxisMarks(position: .leading) { _ in
|
||||||
|
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
|
||||||
|
.foregroundStyle(Color.gray.opacity(0.3))
|
||||||
|
AxisValueLabel()
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
147
cue/src/app.rs
147
cue/src/app.rs
|
|
@ -12,7 +12,7 @@ use tokio::sync::mpsc;
|
||||||
use crate::native_menu::{MenuAction, NativeMenu};
|
use crate::native_menu::{MenuAction, NativeMenu};
|
||||||
use crate::protocol::{
|
use crate::protocol::{
|
||||||
self, AmpPoint, ClPoint, ClResult, Electrode, EisMessage, EisPoint, LpRtia, LsvPoint,
|
self, AmpPoint, ClPoint, ClResult, Electrode, EisMessage, EisPoint, LpRtia, LsvPoint,
|
||||||
PhResult, Rcal, Rtia,
|
OrpResult, OrpSample, PhResult, Rcal, Rtia,
|
||||||
};
|
};
|
||||||
use crate::storage::{self, Session, Storage};
|
use crate::storage::{self, Session, Storage};
|
||||||
use crate::udp::UdpEvent;
|
use crate::udp::UdpEvent;
|
||||||
|
|
@ -50,6 +50,7 @@ pub enum Tab {
|
||||||
Amp,
|
Amp,
|
||||||
Chlorine,
|
Chlorine,
|
||||||
Ph,
|
Ph,
|
||||||
|
Orp,
|
||||||
Calibrate,
|
Calibrate,
|
||||||
Browse,
|
Browse,
|
||||||
}
|
}
|
||||||
|
|
@ -121,6 +122,10 @@ pub enum Message {
|
||||||
/* pH */
|
/* pH */
|
||||||
PhStabilizeChanged(String),
|
PhStabilizeChanged(String),
|
||||||
StartPh,
|
StartPh,
|
||||||
|
/* ORP */
|
||||||
|
OrpStabilizeChanged(String),
|
||||||
|
StartOrp,
|
||||||
|
ClearOrpHistory,
|
||||||
/* Calibration */
|
/* Calibration */
|
||||||
CalVolumeChanged(String),
|
CalVolumeChanged(String),
|
||||||
CalNaclChanged(String),
|
CalNaclChanged(String),
|
||||||
|
|
@ -257,6 +262,12 @@ pub struct App {
|
||||||
ph_result: Option<PhResult>,
|
ph_result: Option<PhResult>,
|
||||||
ph_stabilize: String,
|
ph_stabilize: String,
|
||||||
|
|
||||||
|
/* ORP */
|
||||||
|
orp_result: Option<OrpResult>,
|
||||||
|
orp_stabilize: String,
|
||||||
|
orp_history: Vec<OrpSample>,
|
||||||
|
orp_t0: Option<std::time::Instant>,
|
||||||
|
|
||||||
/* measurement dedup */
|
/* measurement dedup */
|
||||||
current_esp_ts: Option<u32>,
|
current_esp_ts: Option<u32>,
|
||||||
|
|
||||||
|
|
@ -266,6 +277,7 @@ pub struct App {
|
||||||
amp_ref: Option<Vec<AmpPoint>>,
|
amp_ref: Option<Vec<AmpPoint>>,
|
||||||
cl_ref: Option<(Vec<ClPoint>, ClResult)>,
|
cl_ref: Option<(Vec<ClPoint>, ClResult)>,
|
||||||
ph_ref: Option<PhResult>,
|
ph_ref: Option<PhResult>,
|
||||||
|
orp_ref: Option<OrpResult>,
|
||||||
|
|
||||||
/* Device reference collection */
|
/* Device reference collection */
|
||||||
collecting_refs: bool,
|
collecting_refs: bool,
|
||||||
|
|
@ -513,6 +525,11 @@ impl App {
|
||||||
ph_result: None,
|
ph_result: None,
|
||||||
ph_stabilize: "30".into(),
|
ph_stabilize: "30".into(),
|
||||||
|
|
||||||
|
orp_result: None,
|
||||||
|
orp_stabilize: "30".into(),
|
||||||
|
orp_history: Vec::new(),
|
||||||
|
orp_t0: None,
|
||||||
|
|
||||||
current_esp_ts: None,
|
current_esp_ts: None,
|
||||||
|
|
||||||
eis_ref: None,
|
eis_ref: None,
|
||||||
|
|
@ -520,6 +537,7 @@ impl App {
|
||||||
amp_ref: None,
|
amp_ref: None,
|
||||||
cl_ref: None,
|
cl_ref: None,
|
||||||
ph_ref: None,
|
ph_ref: None,
|
||||||
|
orp_ref: None,
|
||||||
|
|
||||||
collecting_refs: false,
|
collecting_refs: false,
|
||||||
ref_mode: None,
|
ref_mode: None,
|
||||||
|
|
@ -649,6 +667,17 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn save_orp(&self, session_id: i64, result: &OrpResult) {
|
||||||
|
let params = serde_json::json!({
|
||||||
|
"stabilize_s": self.orp_stabilize,
|
||||||
|
});
|
||||||
|
if let Ok(mid) = self.storage.create_measurement(session_id, "orp", ¶ms.to_string(), self.current_esp_ts) {
|
||||||
|
if let Ok(j) = serde_json::to_string(result) {
|
||||||
|
let _ = self.storage.add_data_point(mid, 0, &j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, message: Message) -> Task<Message> {
|
pub fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
match message {
|
match message {
|
||||||
Message::DeviceReady(tx) => {
|
Message::DeviceReady(tx) => {
|
||||||
|
|
@ -836,6 +865,22 @@ impl App {
|
||||||
self.ph_result = Some(r);
|
self.ph_result = Some(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
EisMessage::OrpResult(r, esp_ts, _) => {
|
||||||
|
if self.collecting_refs {
|
||||||
|
self.orp_ref = Some(r);
|
||||||
|
} else {
|
||||||
|
if let Some(sid) = self.current_session {
|
||||||
|
self.current_esp_ts = esp_ts;
|
||||||
|
self.save_orp(sid, &r);
|
||||||
|
}
|
||||||
|
let t0 = *self.orp_t0.get_or_insert_with(std::time::Instant::now);
|
||||||
|
let t_s = t0.elapsed().as_secs_f32();
|
||||||
|
self.orp_history.push(OrpSample { t_s, v_mv: r.v_orp_mv });
|
||||||
|
self.status = format!("ORP: {:.1} mV (T={:.1}C)",
|
||||||
|
r.v_orp_mv, r.temp_c);
|
||||||
|
self.orp_result = Some(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
EisMessage::Temperature(t) => {
|
EisMessage::Temperature(t) => {
|
||||||
self.temp_c = t;
|
self.temp_c = t;
|
||||||
}
|
}
|
||||||
|
|
@ -948,7 +993,7 @@ impl App {
|
||||||
Tab::Lsv => self.lsv_data.perform(action),
|
Tab::Lsv => self.lsv_data.perform(action),
|
||||||
Tab::Amp => self.amp_data.perform(action),
|
Tab::Amp => self.amp_data.perform(action),
|
||||||
Tab::Chlorine => self.cl_data.perform(action),
|
Tab::Chlorine => self.cl_data.perform(action),
|
||||||
Tab::Ph | Tab::Calibrate | Tab::Browse => {}
|
Tab::Ph | Tab::Orp | Tab::Calibrate | Tab::Browse => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1056,6 +1101,18 @@ impl App {
|
||||||
self.send_cmd(&protocol::build_sysex_get_temp());
|
self.send_cmd(&protocol::build_sysex_get_temp());
|
||||||
self.send_cmd(&protocol::build_sysex_start_ph(stab));
|
self.send_cmd(&protocol::build_sysex_start_ph(stab));
|
||||||
}
|
}
|
||||||
|
/* ORP */
|
||||||
|
Message::OrpStabilizeChanged(s) => self.orp_stabilize = s,
|
||||||
|
Message::StartOrp => {
|
||||||
|
let stab = self.orp_stabilize.parse::<f32>().unwrap_or(30.0);
|
||||||
|
self.send_cmd(&protocol::build_sysex_get_temp());
|
||||||
|
self.send_cmd(&protocol::build_sysex_start_orp(stab));
|
||||||
|
}
|
||||||
|
Message::ClearOrpHistory => {
|
||||||
|
self.orp_history.clear();
|
||||||
|
self.orp_t0 = None;
|
||||||
|
self.status = "ORP history cleared".into();
|
||||||
|
}
|
||||||
/* Reference baseline */
|
/* Reference baseline */
|
||||||
Message::SetReference => {
|
Message::SetReference => {
|
||||||
match self.tab {
|
match self.tab {
|
||||||
|
|
@ -1083,6 +1140,12 @@ impl App {
|
||||||
self.status = format!("pH reference set ({:.2})", r.ph);
|
self.status = format!("pH reference set ({:.2})", r.ph);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Tab::Orp => {
|
||||||
|
if let Some(r) = &self.orp_result {
|
||||||
|
self.orp_ref = Some(r.clone());
|
||||||
|
self.status = format!("ORP reference set ({:.1} mV)", r.v_orp_mv);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1122,6 +1185,7 @@ impl App {
|
||||||
Tab::Amp => { self.amp_ref = None; self.status = "Amp reference cleared".into(); }
|
Tab::Amp => { self.amp_ref = None; self.status = "Amp reference cleared".into(); }
|
||||||
Tab::Chlorine => { self.cl_ref = None; self.status = "Chlorine reference cleared".into(); }
|
Tab::Chlorine => { self.cl_ref = None; self.status = "Chlorine reference cleared".into(); }
|
||||||
Tab::Ph => { self.ph_ref = None; self.status = "pH reference cleared".into(); }
|
Tab::Ph => { self.ph_ref = None; self.status = "pH reference cleared".into(); }
|
||||||
|
Tab::Orp => { self.orp_ref = None; self.status = "ORP reference cleared".into(); }
|
||||||
Tab::Calibrate | Tab::Browse => {}
|
Tab::Calibrate | Tab::Browse => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1440,6 +1504,7 @@ impl App {
|
||||||
tab_btn("Amperometry", Tab::Amp, self.tab == Tab::Amp),
|
tab_btn("Amperometry", Tab::Amp, self.tab == Tab::Amp),
|
||||||
tab_btn("Chlorine", Tab::Chlorine, self.tab == Tab::Chlorine),
|
tab_btn("Chlorine", Tab::Chlorine, self.tab == Tab::Chlorine),
|
||||||
tab_btn("pH", Tab::Ph, self.tab == Tab::Ph),
|
tab_btn("pH", Tab::Ph, self.tab == Tab::Ph),
|
||||||
|
tab_btn("ORP", Tab::Orp, self.tab == Tab::Orp),
|
||||||
tab_btn("Cal", Tab::Calibrate, self.tab == Tab::Calibrate),
|
tab_btn("Cal", Tab::Calibrate, self.tab == Tab::Calibrate),
|
||||||
tab_btn("Browse", Tab::Browse, self.tab == Tab::Browse),
|
tab_btn("Browse", Tab::Browse, self.tab == Tab::Browse),
|
||||||
]
|
]
|
||||||
|
|
@ -1463,6 +1528,7 @@ impl App {
|
||||||
Tab::Amp => self.amp_ref.is_some(),
|
Tab::Amp => self.amp_ref.is_some(),
|
||||||
Tab::Chlorine => self.cl_ref.is_some(),
|
Tab::Chlorine => self.cl_ref.is_some(),
|
||||||
Tab::Ph => self.ph_ref.is_some(),
|
Tab::Ph => self.ph_ref.is_some(),
|
||||||
|
Tab::Orp => self.orp_ref.is_some(),
|
||||||
Tab::Calibrate | Tab::Browse => false,
|
Tab::Calibrate | Tab::Browse => false,
|
||||||
};
|
};
|
||||||
let has_data = match self.tab {
|
let has_data = match self.tab {
|
||||||
|
|
@ -1471,6 +1537,7 @@ impl App {
|
||||||
Tab::Amp => !self.amp_points.is_empty(),
|
Tab::Amp => !self.amp_points.is_empty(),
|
||||||
Tab::Chlorine => self.cl_result.is_some(),
|
Tab::Chlorine => self.cl_result.is_some(),
|
||||||
Tab::Ph => self.ph_result.is_some(),
|
Tab::Ph => self.ph_result.is_some(),
|
||||||
|
Tab::Orp => self.orp_result.is_some(),
|
||||||
Tab::Calibrate | Tab::Browse => false,
|
Tab::Calibrate | Tab::Browse => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1554,6 +1621,8 @@ impl App {
|
||||||
self.view_browse_body()
|
self.view_browse_body()
|
||||||
} else if self.tab == Tab::Ph {
|
} else if self.tab == Tab::Ph {
|
||||||
self.view_ph_body()
|
self.view_ph_body()
|
||||||
|
} else if self.tab == Tab::Orp {
|
||||||
|
self.view_orp_body()
|
||||||
} else if self.tab == Tab::Calibrate {
|
} else if self.tab == Tab::Calibrate {
|
||||||
self.view_cal_body()
|
self.view_cal_body()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1858,6 +1927,25 @@ impl App {
|
||||||
.align_y(iced::Alignment::End)
|
.align_y(iced::Alignment::End)
|
||||||
.into(),
|
.into(),
|
||||||
|
|
||||||
|
Tab::Orp => row![
|
||||||
|
column![
|
||||||
|
text("Stabilize s").size(12),
|
||||||
|
text_input("30", &self.orp_stabilize).on_input(Message::OrpStabilizeChanged).width(80),
|
||||||
|
].spacing(2),
|
||||||
|
button(text("Measure ORP").size(13))
|
||||||
|
.style(style_action())
|
||||||
|
.padding([6, 16])
|
||||||
|
.on_press(Message::StartOrp),
|
||||||
|
button(text("Clear").size(13))
|
||||||
|
.style(style_action())
|
||||||
|
.padding([6, 12])
|
||||||
|
.on_press(Message::ClearOrpHistory),
|
||||||
|
text(format!("n={}", self.orp_history.len())).size(12),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Alignment::End)
|
||||||
|
.into(),
|
||||||
|
|
||||||
Tab::Calibrate | Tab::Browse => row![].into(),
|
Tab::Calibrate | Tab::Browse => row![].into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1961,7 +2049,7 @@ impl App {
|
||||||
|
|
||||||
col.into()
|
col.into()
|
||||||
}
|
}
|
||||||
Tab::Ph | Tab::Calibrate | Tab::Browse => text("").into(),
|
Tab::Ph | Tab::Orp | Tab::Calibrate | Tab::Browse => text("").into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1971,7 +2059,7 @@ impl App {
|
||||||
Tab::Lsv => &self.lsv_data,
|
Tab::Lsv => &self.lsv_data,
|
||||||
Tab::Amp => &self.amp_data,
|
Tab::Amp => &self.amp_data,
|
||||||
Tab::Chlorine => &self.cl_data,
|
Tab::Chlorine => &self.cl_data,
|
||||||
Tab::Ph | Tab::Calibrate | Tab::Browse => return text("").into(),
|
Tab::Ph | Tab::Orp | Tab::Calibrate | Tab::Browse => return text("").into(),
|
||||||
};
|
};
|
||||||
text_editor(content)
|
text_editor(content)
|
||||||
.on_action(Message::DataAction)
|
.on_action(Message::DataAction)
|
||||||
|
|
@ -2225,6 +2313,39 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn view_orp_body(&self) -> Element<'_, Message> {
|
||||||
|
let header: Element<'_, Message> = if let Some(r) = &self.orp_result {
|
||||||
|
let mut col = column![
|
||||||
|
text(format!("ORP: {:.0} mV", r.v_orp_mv)).size(36),
|
||||||
|
text(format!("Temp: {:.1} C", r.temp_c)).size(14),
|
||||||
|
].spacing(4);
|
||||||
|
if let Some(ref_r) = &self.orp_ref {
|
||||||
|
let d_v = r.v_orp_mv - ref_r.v_orp_mv;
|
||||||
|
col = col.push(text(format!(
|
||||||
|
"vs Ref: dORP={:+.1} mV (ref={:.0} mV)",
|
||||||
|
d_v, ref_r.v_orp_mv
|
||||||
|
)).size(14));
|
||||||
|
}
|
||||||
|
col.into()
|
||||||
|
} else {
|
||||||
|
column![
|
||||||
|
text("No measurement yet").size(16),
|
||||||
|
text("Open-circuit potential vs AgCl reference. Above ~650 mV indicates sanitary water.").size(12),
|
||||||
|
].spacing(4).into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let plot = canvas(crate::plot::OrpPlot { samples: &self.orp_history })
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill);
|
||||||
|
|
||||||
|
column![
|
||||||
|
header,
|
||||||
|
plot,
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
fn view_sysinfo(&self) -> Element<'_, Message> {
|
fn view_sysinfo(&self) -> Element<'_, Message> {
|
||||||
container(
|
container(
|
||||||
column![
|
column![
|
||||||
|
|
@ -2515,6 +2636,15 @@ impl App {
|
||||||
self.tab = Tab::Ph;
|
self.tab = Tab::Ph;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"orp" => {
|
||||||
|
if let Some(dp) = pts.first()
|
||||||
|
&& let Ok(r) = serde_json::from_str::<OrpResult>(&dp.data_json)
|
||||||
|
{
|
||||||
|
self.status = format!("Loaded ORP: {:.0} mV", r.v_orp_mv);
|
||||||
|
self.orp_result = Some(r);
|
||||||
|
self.tab = Tab::Orp;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2570,6 +2700,15 @@ impl App {
|
||||||
self.tab = Tab::Ph;
|
self.tab = Tab::Ph;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"orp" => {
|
||||||
|
if let Some(dp) = pts.first()
|
||||||
|
&& let Ok(r) = serde_json::from_str::<OrpResult>(&dp.data_json)
|
||||||
|
{
|
||||||
|
self.status = format!("ORP ref loaded: {:.0} mV", r.v_orp_mv);
|
||||||
|
self.orp_ref = Some(r);
|
||||||
|
self.tab = Tab::Orp;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
184
cue/src/plot.rs
184
cue/src/plot.rs
|
|
@ -4,7 +4,7 @@ use iced::mouse;
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::lsv_analysis::{LsvPeak, PeakKind};
|
use crate::lsv_analysis::{LsvPeak, PeakKind};
|
||||||
use crate::protocol::{AmpPoint, ClPoint, EisPoint, LsvPoint};
|
use crate::protocol::{AmpPoint, ClPoint, EisPoint, LsvPoint, OrpSample};
|
||||||
|
|
||||||
const MARGIN_L: f32 = 55.0;
|
const MARGIN_L: f32 = 55.0;
|
||||||
const MARGIN_R: f32 = 15.0;
|
const MARGIN_R: f32 = 15.0;
|
||||||
|
|
@ -1210,6 +1210,188 @@ impl<'a> canvas::Program<Message> for AmperogramPlot<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- ORP history ---- */
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct OrpState {
|
||||||
|
xv: Option<Vr>,
|
||||||
|
yv: Option<Vr>,
|
||||||
|
left_drag: Option<(Point, Vr, Vr)>,
|
||||||
|
right_drag: Option<(Point, Vr, Vr)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OrpPlot<'a> {
|
||||||
|
pub samples: &'a [OrpSample],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OrpPlot<'_> {
|
||||||
|
fn auto_view(&self) -> Option<(Vr, Vr)> {
|
||||||
|
let valid: Vec<_> = self.samples.iter()
|
||||||
|
.filter(|s| s.t_s.is_finite() && s.v_mv.is_finite())
|
||||||
|
.collect();
|
||||||
|
if valid.is_empty() { return None; }
|
||||||
|
let (xlo, xhi) = valid.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), s| {
|
||||||
|
(lo.min(s.t_s), hi.max(s.t_s))
|
||||||
|
});
|
||||||
|
let (ylo, yhi) = valid.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), s| {
|
||||||
|
(lo.min(s.v_mv), hi.max(s.v_mv))
|
||||||
|
});
|
||||||
|
let xpad = (xhi - xlo).max(10.0) * 0.05;
|
||||||
|
let ypad = (yhi - ylo).max(10.0) * 0.15;
|
||||||
|
Some((Vr::new(xlo - xpad, xhi + xpad), Vr::new(ylo - ypad, yhi + ypad)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effective_ranges(&self, state: &OrpState) -> (Vr, Vr) {
|
||||||
|
let auto = self.auto_view().unwrap_or((Vr::new(0.0, 60.0), Vr::new(0.0, 1000.0)));
|
||||||
|
(state.xv.unwrap_or(auto.0), state.yv.unwrap_or(auto.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> canvas::Program<Message> for OrpPlot<'a> {
|
||||||
|
type State = OrpState;
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&self, state: &mut OrpState, event: Event,
|
||||||
|
bounds: Rectangle, cursor: mouse::Cursor,
|
||||||
|
) -> (canvas::event::Status, Option<Message>) {
|
||||||
|
if let Event::Mouse(mouse::Event::ButtonReleased(_)) = &event {
|
||||||
|
let was_right = state.right_drag.is_some();
|
||||||
|
if was_right {
|
||||||
|
if let Some(pos) = cursor.position_in(bounds) {
|
||||||
|
let (start, _, _) = state.right_drag.unwrap();
|
||||||
|
let dist = ((pos.x - start.x).powi(2) + (pos.y - start.y).powi(2)).sqrt();
|
||||||
|
if dist < 3.0 { state.xv = None; state.yv = None; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.left_drag = None;
|
||||||
|
state.right_drag = None;
|
||||||
|
if was_right { return (canvas::event::Status::Captured, None); }
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(pos) = cursor.position_in(bounds) else {
|
||||||
|
return (canvas::event::Status::Ignored, None);
|
||||||
|
};
|
||||||
|
let xl = MARGIN_L;
|
||||||
|
let xr = bounds.width - MARGIN_R;
|
||||||
|
let yt = MARGIN_T;
|
||||||
|
let yb = bounds.height - MARGIN_B;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
|
||||||
|
let dy = match delta {
|
||||||
|
mouse::ScrollDelta::Lines { y, .. } => y,
|
||||||
|
mouse::ScrollDelta::Pixels { y, .. } => y / 40.0,
|
||||||
|
};
|
||||||
|
let factor = ZOOM_FACTOR.powf(dy);
|
||||||
|
let (mut xv, mut yv) = self.effective_ranges(state);
|
||||||
|
xv.zoom_at(factor, screen_frac(pos.x, xl, xr));
|
||||||
|
yv.zoom_at(factor, 1.0 - screen_frac(pos.y, yt, yb));
|
||||||
|
state.xv = Some(xv); state.yv = Some(yv);
|
||||||
|
(canvas::event::Status::Captured, None)
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||||
|
let (xv, yv) = self.effective_ranges(state);
|
||||||
|
state.left_drag = Some((pos, xv, yv));
|
||||||
|
(canvas::event::Status::Captured, None)
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
|
||||||
|
let (xv, yv) = self.effective_ranges(state);
|
||||||
|
state.right_drag = Some((pos, xv, yv));
|
||||||
|
(canvas::event::Status::Captured, None)
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
||||||
|
if let Some((start, sx, sy)) = state.left_drag {
|
||||||
|
let dx = (pos.x - start.x) / (xr - xl);
|
||||||
|
let dy = (pos.y - start.y) / (yb - yt);
|
||||||
|
let mut xv = sx; xv.pan_frac(dx);
|
||||||
|
let mut yv = sy; yv.pan_frac(-dy);
|
||||||
|
state.xv = Some(xv); state.yv = Some(yv);
|
||||||
|
return (canvas::event::Status::Captured, None);
|
||||||
|
}
|
||||||
|
if let Some((start, sx, sy)) = state.right_drag {
|
||||||
|
let dx = pos.x - start.x;
|
||||||
|
let dy = pos.y - start.y;
|
||||||
|
let xf = 2.0_f32.powf(dx / DRAG_ZOOM_RATE);
|
||||||
|
let yf = 2.0_f32.powf(-dy / DRAG_ZOOM_RATE);
|
||||||
|
let mut xv = sx; xv.zoom_at(xf, screen_frac(start.x, xl, xr));
|
||||||
|
let mut yv = sy; yv.zoom_at(yf, 1.0 - screen_frac(start.y, yt, yb));
|
||||||
|
state.xv = Some(xv); state.yv = Some(yv);
|
||||||
|
return (canvas::event::Status::Captured, None);
|
||||||
|
}
|
||||||
|
(canvas::event::Status::Ignored, None)
|
||||||
|
}
|
||||||
|
_ => (canvas::event::Status::Ignored, None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self, state: &OrpState, renderer: &Renderer, _theme: &Theme,
|
||||||
|
bounds: Rectangle, cursor: mouse::Cursor,
|
||||||
|
) -> Vec<Geometry> {
|
||||||
|
let mut frame = Frame::new(renderer, bounds.size());
|
||||||
|
let (w, h) = (bounds.width, bounds.height);
|
||||||
|
|
||||||
|
if self.samples.is_empty() {
|
||||||
|
dt(&mut frame, Point::new(w / 2.0 - 35.0, h / 2.0),
|
||||||
|
"No samples yet", COL_DIM, 13.0);
|
||||||
|
return vec![frame.into_geometry()];
|
||||||
|
}
|
||||||
|
|
||||||
|
let xl = MARGIN_L;
|
||||||
|
let xr = w - MARGIN_R;
|
||||||
|
let yt = MARGIN_T;
|
||||||
|
let yb = h - MARGIN_B;
|
||||||
|
|
||||||
|
let (xv, yv) = self.effective_ranges(state);
|
||||||
|
|
||||||
|
let x_step = nice_step(xv.span(), 5);
|
||||||
|
if x_step > 0.0 {
|
||||||
|
let mut g = (xv.lo / x_step).ceil() * x_step;
|
||||||
|
while g <= xv.hi {
|
||||||
|
let x = lerp(g, xv.lo, xv.hi, xl, xr);
|
||||||
|
dl(&mut frame, Point::new(x, yt), Point::new(x, yb), COL_GRID, 0.5);
|
||||||
|
dt(&mut frame, Point::new(x - 10.0, yb + 3.0), &format!("{:.0}", g), COL_DIM, 9.0);
|
||||||
|
g += x_step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let y_step = nice_step(yv.span(), 4);
|
||||||
|
if y_step > 0.0 {
|
||||||
|
let mut g = (yv.lo / y_step).ceil() * y_step;
|
||||||
|
while g <= yv.hi {
|
||||||
|
let y = lerp(g, yv.hi, yv.lo, yt, yb);
|
||||||
|
dl(&mut frame, Point::new(xl, y), Point::new(xr, y), COL_GRID, 0.5);
|
||||||
|
dt(&mut frame, Point::new(2.0, y - 5.0), &format!("{:.0}", g), COL_PH, 9.0);
|
||||||
|
g += y_step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dt(&mut frame, Point::new(2.0, yt - 2.0), "ORP (mV)", COL_PH, 10.0);
|
||||||
|
dt(&mut frame, Point::new((xl + xr) / 2.0 - 10.0, yb + 3.0), "t (s)", COL_PH, 10.0);
|
||||||
|
|
||||||
|
let pts: Vec<Point> = self.samples.iter().map(|s| Point::new(
|
||||||
|
lerp(s.t_s, xv.lo, xv.hi, xl, xr),
|
||||||
|
lerp(s.v_mv, yv.hi, yv.lo, yt, yb),
|
||||||
|
)).collect();
|
||||||
|
draw_polyline(&mut frame, &pts, COL_PH, 2.0);
|
||||||
|
draw_dots(&mut frame, &pts, COL_PH, 3.0);
|
||||||
|
|
||||||
|
if let Some(pos) = cursor.position_in(bounds) {
|
||||||
|
if pos.x >= xl && pos.x <= xr && pos.y >= yt && pos.y <= yb {
|
||||||
|
let t = lerp(pos.x, xl, xr, xv.lo, xv.hi);
|
||||||
|
let v = lerp(pos.y, yt, yb, yv.hi, yv.lo);
|
||||||
|
dl(&mut frame, Point::new(pos.x, yt), Point::new(pos.x, yb),
|
||||||
|
Color { a: 0.3, ..COL_AXIS }, 1.0);
|
||||||
|
dl(&mut frame, Point::new(xl, pos.y), Point::new(xr, pos.y),
|
||||||
|
Color { a: 0.3, ..COL_AXIS }, 1.0);
|
||||||
|
dt(&mut frame, Point::new(pos.x + 4.0, pos.y - 14.0),
|
||||||
|
&format!("{:.1}s, {:.0}mV", t, v), COL_AXIS, 10.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![frame.into_geometry()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Chlorine (multi-step chronoamperometry) ---- */
|
/* ---- Chlorine (multi-step chronoamperometry) ---- */
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,11 @@ pub const RSP_CL_RESULT: u8 = 0x0D;
|
||||||
pub const RSP_CL_END: u8 = 0x0E;
|
pub const RSP_CL_END: u8 = 0x0E;
|
||||||
pub const RSP_PH_RESULT: u8 = 0x0F;
|
pub const RSP_PH_RESULT: u8 = 0x0F;
|
||||||
pub const RSP_TEMP: u8 = 0x10;
|
pub const RSP_TEMP: u8 = 0x10;
|
||||||
|
pub const RSP_CELL_K: u8 = 0x11;
|
||||||
|
pub const RSP_ORP_RESULT: u8 = 0x12;
|
||||||
pub const RSP_REF_FRAME: u8 = 0x20;
|
pub const RSP_REF_FRAME: u8 = 0x20;
|
||||||
pub const RSP_REF_LP_RANGE: u8 = 0x21;
|
pub const RSP_REF_LP_RANGE: u8 = 0x21;
|
||||||
pub const RSP_REFS_DONE: u8 = 0x22;
|
pub const RSP_REFS_DONE: u8 = 0x22;
|
||||||
pub const RSP_CELL_K: u8 = 0x11;
|
|
||||||
pub const RSP_REF_STATUS: u8 = 0x23;
|
pub const RSP_REF_STATUS: u8 = 0x23;
|
||||||
pub const RSP_CL_FACTOR: u8 = 0x24;
|
pub const RSP_CL_FACTOR: u8 = 0x24;
|
||||||
pub const RSP_PH_CAL: u8 = 0x25;
|
pub const RSP_PH_CAL: u8 = 0x25;
|
||||||
|
|
@ -47,6 +48,7 @@ pub const CMD_GET_TEMP: u8 = 0x17;
|
||||||
pub const CMD_START_CL: u8 = 0x23;
|
pub const CMD_START_CL: u8 = 0x23;
|
||||||
pub const CMD_START_PH: u8 = 0x24;
|
pub const CMD_START_PH: u8 = 0x24;
|
||||||
pub const CMD_START_CLEAN: u8 = 0x25;
|
pub const CMD_START_CLEAN: u8 = 0x25;
|
||||||
|
pub const CMD_START_ORP: u8 = 0x2A;
|
||||||
pub const CMD_SET_CELL_K: u8 = 0x28;
|
pub const CMD_SET_CELL_K: u8 = 0x28;
|
||||||
pub const CMD_GET_CELL_K: u8 = 0x29;
|
pub const CMD_GET_CELL_K: u8 = 0x29;
|
||||||
pub const CMD_SET_CL_FACTOR: u8 = 0x33;
|
pub const CMD_SET_CL_FACTOR: u8 = 0x33;
|
||||||
|
|
@ -236,6 +238,18 @@ pub struct PhResult {
|
||||||
pub temp_c: f32,
|
pub temp_c: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct OrpResult {
|
||||||
|
pub v_orp_mv: f32,
|
||||||
|
pub temp_c: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct OrpSample {
|
||||||
|
pub t_s: f32,
|
||||||
|
pub v_mv: f32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EisConfig {
|
pub struct EisConfig {
|
||||||
pub freq_start: f32,
|
pub freq_start: f32,
|
||||||
|
|
@ -265,6 +279,7 @@ pub enum EisMessage {
|
||||||
ClResult(ClResult),
|
ClResult(ClResult),
|
||||||
ClEnd,
|
ClEnd,
|
||||||
PhResult(PhResult, Option<u32>, Option<u16>),
|
PhResult(PhResult, Option<u32>, Option<u16>),
|
||||||
|
OrpResult(OrpResult, Option<u32>, Option<u16>),
|
||||||
Temperature(f32),
|
Temperature(f32),
|
||||||
RefFrame { mode: u8, rtia_idx: u8 },
|
RefFrame { mode: u8, rtia_idx: u8 },
|
||||||
RefLpRange { mode: u8, low_idx: u8, high_idx: u8 },
|
RefLpRange { mode: u8, low_idx: u8, high_idx: u8 },
|
||||||
|
|
@ -446,6 +461,16 @@ pub fn parse_sysex(data: &[u8]) -> Option<EisMessage> {
|
||||||
temp_c: decode_float(&p[10..15]),
|
temp_c: decode_float(&p[10..15]),
|
||||||
}, ts, mid))
|
}, ts, mid))
|
||||||
}
|
}
|
||||||
|
RSP_ORP_RESULT if data.len() >= 12 => {
|
||||||
|
let p = &data[2..];
|
||||||
|
let (ts, mid) = if p.len() >= 18 {
|
||||||
|
(Some(decode_u32(&p[10..15])), Some(decode_u16(&p[15..18])))
|
||||||
|
} else { (None, None) };
|
||||||
|
Some(EisMessage::OrpResult(OrpResult {
|
||||||
|
v_orp_mv: decode_float(&p[0..5]),
|
||||||
|
temp_c: decode_float(&p[5..10]),
|
||||||
|
}, ts, mid))
|
||||||
|
}
|
||||||
RSP_REF_FRAME if data.len() >= 4 => {
|
RSP_REF_FRAME if data.len() >= 4 => {
|
||||||
Some(EisMessage::RefFrame { mode: data[2], rtia_idx: data[3] })
|
Some(EisMessage::RefFrame { mode: data[2], rtia_idx: data[3] })
|
||||||
}
|
}
|
||||||
|
|
@ -580,6 +605,13 @@ pub fn build_sysex_start_ph(stabilize_s: f32) -> Vec<u8> {
|
||||||
sx
|
sx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_sysex_start_orp(stabilize_s: f32) -> Vec<u8> {
|
||||||
|
let mut sx = vec![0xF0, SYSEX_MFR, CMD_START_ORP];
|
||||||
|
sx.extend_from_slice(&encode_float(stabilize_s));
|
||||||
|
sx.push(0xF7);
|
||||||
|
sx
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_sysex_start_clean(v_mv: f32, duration_s: f32) -> Vec<u8> {
|
pub fn build_sysex_start_clean(v_mv: f32, duration_s: f32) -> Vec<u8> {
|
||||||
let mut sx = vec![0xF0, SYSEX_MFR, CMD_START_CLEAN];
|
let mut sx = vec![0xF0, SYSEX_MFR, CMD_START_CLEAN];
|
||||||
sx.extend_from_slice(&encode_float(v_mv));
|
sx.extend_from_slice(&encode_float(v_mv));
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,10 @@ fn data_point_to_toml(mtype: &str, jv: &serde_json::Value) -> Option<Table> {
|
||||||
t.insert("pH".into(), toml_f(obj, "ph")?);
|
t.insert("pH".into(), toml_f(obj, "ph")?);
|
||||||
t.insert("Temperature (C)".into(), toml_f(obj, "temp_c")?);
|
t.insert("Temperature (C)".into(), toml_f(obj, "temp_c")?);
|
||||||
}
|
}
|
||||||
|
"orp" => {
|
||||||
|
t.insert("ORP (mV)".into(), toml_f(obj, "v_orp_mv")?);
|
||||||
|
t.insert("Temperature (C)".into(), toml_f(obj, "temp_c")?);
|
||||||
|
}
|
||||||
_ => return None,
|
_ => return None,
|
||||||
}
|
}
|
||||||
Some(t)
|
Some(t)
|
||||||
|
|
@ -427,6 +431,10 @@ fn toml_data_row_to_json(mtype: &str, row: &Table) -> serde_json::Value {
|
||||||
set_f(&mut obj, "ph", row, "pH");
|
set_f(&mut obj, "ph", row, "pH");
|
||||||
set_f(&mut obj, "temp_c", row, "Temperature (C)");
|
set_f(&mut obj, "temp_c", row, "Temperature (C)");
|
||||||
}
|
}
|
||||||
|
"orp" => {
|
||||||
|
set_f(&mut obj, "v_orp_mv", row, "ORP (mV)");
|
||||||
|
set_f(&mut obj, "temp_c", row, "Temperature (C)");
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
serde_json::Value::Object(obj)
|
serde_json::Value::Object(obj)
|
||||||
|
|
|
||||||
11
main/echem.c
11
main/echem.c
|
|
@ -645,3 +645,14 @@ int echem_ph_ocp(const PhConfig *cfg, PhResult *result)
|
||||||
AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
|
AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int echem_orp_read(const OrpConfig *cfg, OrpResult *result)
|
||||||
|
{
|
||||||
|
PhResult ph_res;
|
||||||
|
int rc = echem_ph_ocp(cfg, &ph_res);
|
||||||
|
if (rc != 0) return rc;
|
||||||
|
result->v_orp_mv = ph_res.v_ocp_mv;
|
||||||
|
result->temp_c = ph_res.temp_c;
|
||||||
|
printf("ORP: %.1f mV @ %.1f C\n", result->v_orp_mv, result->temp_c);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,14 @@ typedef struct {
|
||||||
float temp_c; /* temperature used */
|
float temp_c; /* temperature used */
|
||||||
} PhResult;
|
} PhResult;
|
||||||
|
|
||||||
|
/* ORP: raw open-circuit potential without pH calibration */
|
||||||
|
typedef PhConfig OrpConfig;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float v_orp_mv; /* raw OCP: V(SE0) - V(RE0) in mV */
|
||||||
|
float temp_c; /* temperature at measurement */
|
||||||
|
} OrpResult;
|
||||||
|
|
||||||
typedef int (*lsv_point_cb_t)(uint16_t idx, float v_mv, float i_ua);
|
typedef int (*lsv_point_cb_t)(uint16_t idx, float v_mv, float i_ua);
|
||||||
typedef int (*amp_point_cb_t)(uint16_t idx, float t_ms, float i_ua);
|
typedef int (*amp_point_cb_t)(uint16_t idx, float t_ms, float i_ua);
|
||||||
typedef int (*cl_point_cb_t)(uint16_t idx, float t_ms, float i_ua, uint8_t phase);
|
typedef int (*cl_point_cb_t)(uint16_t idx, float t_ms, float i_ua, uint8_t phase);
|
||||||
|
|
@ -113,5 +121,6 @@ int echem_lsv(const LSVConfig *cfg, LSVPoint *out, uint32_t max_points, lsv_poin
|
||||||
int echem_amp(const AmpConfig *cfg, AmpPoint *out, uint32_t max_points, amp_point_cb_t cb);
|
int echem_amp(const AmpConfig *cfg, AmpPoint *out, uint32_t max_points, amp_point_cb_t cb);
|
||||||
int echem_chlorine(const ClConfig *cfg, ClPoint *out, uint32_t max_points, ClResult *result, cl_point_cb_t cb);
|
int echem_chlorine(const ClConfig *cfg, ClPoint *out, uint32_t max_points, ClResult *result, cl_point_cb_t cb);
|
||||||
int echem_ph_ocp(const PhConfig *cfg, PhResult *result);
|
int echem_ph_ocp(const PhConfig *cfg, PhResult *result);
|
||||||
|
int echem_orp_read(const OrpConfig *cfg, OrpResult *result);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
229
main/eis.c
229
main/eis.c
|
|
@ -238,9 +238,11 @@ static void configure_freq(float freq_hz)
|
||||||
fp.DftSrc = DFTSRC_ADCRAW;
|
fp.DftSrc = DFTSRC_ADCRAW;
|
||||||
fp.ADCSinc3Osr = ADCSINC3OSR_2;
|
fp.ADCSinc3Osr = ADCSINC3OSR_2;
|
||||||
fp.ADCSinc2Osr = 0;
|
fp.ADCSinc2Osr = 0;
|
||||||
fp.DftNum = DFTNUM_4096;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* widest DFT window to suppress non-coherent leakage */
|
||||||
|
fp.DftNum = DFTNUM_16384;
|
||||||
|
|
||||||
AD5940_WriteReg(REG_AFE_WGFCW,
|
AD5940_WriteReg(REG_AFE_WGFCW,
|
||||||
AD5940_WGFreqWordCal(freq_hz, ctx.sys_clk));
|
AD5940_WGFreqWordCal(freq_hz, ctx.sys_clk));
|
||||||
|
|
||||||
|
|
@ -271,8 +273,17 @@ static int32_t sign_extend_18(uint32_t v)
|
||||||
return (v & (1UL << 17)) ? (int32_t)(v | 0xFFFC0000UL) : (int32_t)v;
|
return (v & (1UL << 17)) ? (int32_t)(v | 0xFFFC0000UL) : (int32_t)v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* settles for a fixed count of excitation periods, floored for high-frequency overhead */
|
||||||
|
static void settle(float freq_hz, float cycles, uint32_t floor_us)
|
||||||
|
{
|
||||||
|
float us = cycles * 1e6f / freq_hz;
|
||||||
|
uint32_t d_us = (us > (float)floor_us) ? (uint32_t)us : floor_us;
|
||||||
|
AD5940_Delay10us(d_us / 10);
|
||||||
|
}
|
||||||
|
|
||||||
/* paired DFT: two measurements under continuous WG excitation */
|
/* paired DFT: two measurements under continuous WG excitation */
|
||||||
static void dft_measure_pair(
|
static void dft_measure_pair(
|
||||||
|
float freq_hz,
|
||||||
uint32_t mux1_p, uint32_t mux1_n, iImpCar_Type *out1,
|
uint32_t mux1_p, uint32_t mux1_n, iImpCar_Type *out1,
|
||||||
uint32_t mux2_p, uint32_t mux2_n, iImpCar_Type *out2)
|
uint32_t mux2_p, uint32_t mux2_n, iImpCar_Type *out2)
|
||||||
{
|
{
|
||||||
|
|
@ -284,7 +295,7 @@ static void dft_measure_pair(
|
||||||
|
|
||||||
AD5940_ADCMuxCfgS(mux1_p, mux1_n);
|
AD5940_ADCMuxCfgS(mux1_p, mux1_n);
|
||||||
AD5940_AFECtrlS(AFECTRL_WG | AFECTRL_ADCPWR, bTRUE);
|
AD5940_AFECtrlS(AFECTRL_WG | AFECTRL_ADCPWR, bTRUE);
|
||||||
AD5940_Delay10us(25);
|
settle(freq_hz, 2.0f, 100);
|
||||||
|
|
||||||
AD5940_ClrMCUIntFlag();
|
AD5940_ClrMCUIntFlag();
|
||||||
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT, bTRUE);
|
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT, bTRUE);
|
||||||
|
|
@ -297,12 +308,12 @@ static void dft_measure_pair(
|
||||||
out1->Image = sign_extend_18(AD5940_ReadAfeResult(AFERESULT_DFTIMAGE));
|
out1->Image = sign_extend_18(AD5940_ReadAfeResult(AFERESULT_DFTIMAGE));
|
||||||
out1->Image = -out1->Image;
|
out1->Image = -out1->Image;
|
||||||
|
|
||||||
/* switch ADC mux, flush stale pipeline, short settle */
|
/* switch ADC mux, flush stale pipeline, settle one period */
|
||||||
AD5940_ADCMuxCfgS(mux2_p, mux2_n);
|
AD5940_ADCMuxCfgS(mux2_p, mux2_n);
|
||||||
AD5940_ReadAfeResult(AFERESULT_DFTREAL);
|
AD5940_ReadAfeResult(AFERESULT_DFTREAL);
|
||||||
AD5940_ReadAfeResult(AFERESULT_DFTIMAGE);
|
AD5940_ReadAfeResult(AFERESULT_DFTIMAGE);
|
||||||
AD5940_INTCClrFlag(AFEINTSRC_DFTRDY);
|
AD5940_INTCClrFlag(AFEINTSRC_DFTRDY);
|
||||||
AD5940_Delay10us(5);
|
settle(freq_hz, 1.0f, 50);
|
||||||
|
|
||||||
AD5940_ClrMCUIntFlag();
|
AD5940_ClrMCUIntFlag();
|
||||||
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT, bTRUE);
|
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT, bTRUE);
|
||||||
|
|
@ -317,10 +328,10 @@ static void dft_measure_pair(
|
||||||
out2->Image = -out2->Image;
|
out2->Image = -out2->Image;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fImpCar_Type measure_rtia(iImpCar_Type *out_hstia)
|
static fImpCar_Type measure_rtia(float freq_hz, iImpCar_Type *out_hstia)
|
||||||
{
|
{
|
||||||
iImpCar_Type v_rcal, v_raw;
|
iImpCar_Type v_rcal, v_raw;
|
||||||
dft_measure_pair(
|
dft_measure_pair(freq_hz,
|
||||||
ADCMUXP_P_NODE, ADCMUXN_N_NODE, &v_rcal,
|
ADCMUXP_P_NODE, ADCMUXN_N_NODE, &v_rcal,
|
||||||
ADCMUXP_HSTIA_P, ADCMUXN_HSTIA_N, &v_raw);
|
ADCMUXP_HSTIA_P, ADCMUXN_HSTIA_N, &v_raw);
|
||||||
if (out_hstia) *out_hstia = v_raw;
|
if (out_hstia) *out_hstia = v_raw;
|
||||||
|
|
@ -336,142 +347,88 @@ static fImpCar_Type measure_rtia(iImpCar_Type *out_hstia)
|
||||||
|
|
||||||
int eis_measure_point(float freq_hz, EISPoint *out)
|
int eis_measure_point(float freq_hz, EISPoint *out)
|
||||||
{
|
{
|
||||||
configure_freq(freq_hz);
|
configure_freq(freq_hz);
|
||||||
|
|
||||||
SWMatrixCfg_Type sw;
|
SWMatrixCfg_Type sw;
|
||||||
iImpCar_Type v_tia, v_sense;
|
iImpCar_Type v_tia, v_sense;
|
||||||
|
|
||||||
/* switch to RCAL before power-up */
|
/* RCAL reference before power-up */
|
||||||
sw.Dswitch = ctx.rcal_sw_d;
|
sw.Dswitch = ctx.rcal_sw_d;
|
||||||
sw.Pswitch = ctx.rcal_sw_p;
|
sw.Pswitch = ctx.rcal_sw_p;
|
||||||
sw.Nswitch = ctx.rcal_sw_n;
|
sw.Nswitch = ctx.rcal_sw_n;
|
||||||
sw.Tswitch = ctx.rcal_sw_t;
|
sw.Tswitch = ctx.rcal_sw_t;
|
||||||
AD5940_SWMatrixCfgS(&sw);
|
AD5940_SWMatrixCfgS(&sw);
|
||||||
|
|
||||||
AD5940_AFECtrlS(AFECTRL_HPREFPWR | AFECTRL_HSTIAPWR | AFECTRL_INAMPPWR |
|
AD5940_AFECtrlS(AFECTRL_HPREFPWR | AFECTRL_HSTIAPWR | AFECTRL_INAMPPWR |
|
||||||
AFECTRL_EXTBUFPWR | AFECTRL_DACREFPWR | AFECTRL_HSDACPWR |
|
AFECTRL_EXTBUFPWR | AFECTRL_DACREFPWR | AFECTRL_HSDACPWR |
|
||||||
AFECTRL_SINC2NOTCH, bTRUE);
|
AFECTRL_SINC2NOTCH, bTRUE);
|
||||||
|
|
||||||
/* RCAL before — capture raw HSTIA DFT for ratiometric diagnostic */
|
/* RCAL reference: raw HSTIA DFT plus measured RTIA */
|
||||||
iImpCar_Type rcal_hstia;
|
iImpCar_Type rcal_hstia;
|
||||||
fImpCar_Type rtia_before = measure_rtia(&rcal_hstia);
|
fImpCar_Type rtia = measure_rtia(freq_hz, &rcal_hstia);
|
||||||
|
|
||||||
/* DUT forward */
|
/* DUT: raw HSTIA DFT */
|
||||||
sw.Dswitch = ctx.dut_sw_d;
|
sw.Dswitch = ctx.dut_sw_d;
|
||||||
sw.Pswitch = ctx.dut_sw_p;
|
sw.Pswitch = ctx.dut_sw_p;
|
||||||
sw.Nswitch = ctx.dut_sw_n;
|
sw.Nswitch = ctx.dut_sw_n;
|
||||||
sw.Tswitch = ctx.dut_sw_t;
|
sw.Tswitch = ctx.dut_sw_t;
|
||||||
AD5940_SWMatrixCfgS(&sw);
|
AD5940_SWMatrixCfgS(&sw);
|
||||||
AD5940_Delay10us(50);
|
settle(freq_hz, 2.0f, 200);
|
||||||
|
|
||||||
dft_measure_pair(ADCMUXP_HSTIA_P, ADCMUXN_HSTIA_N, &v_tia,
|
dft_measure_pair(freq_hz,
|
||||||
ctx.dut_mux_vp, ctx.dut_mux_vn, &v_sense);
|
ADCMUXP_HSTIA_P, ADCMUXN_HSTIA_N, &v_tia,
|
||||||
iImpCar_Type dut_hstia_raw = v_tia;
|
ctx.dut_mux_vp, ctx.dut_mux_vn, &v_sense);
|
||||||
v_tia.Real = -v_tia.Real;
|
iImpCar_Type dut_hstia_raw = v_tia;
|
||||||
v_tia.Image = -v_tia.Image;
|
(void)v_sense;
|
||||||
|
|
||||||
iImpCar_Type v_tia_fwd = v_tia;
|
/* power down, open switches */
|
||||||
iImpCar_Type v_sense_fwd = v_sense;
|
AD5940_AFECtrlS(AFECTRL_WG | AFECTRL_ADCPWR | AFECTRL_ADCCNV |
|
||||||
|
AFECTRL_DFT | AFECTRL_SINC2NOTCH | AFECTRL_HSDACPWR |
|
||||||
|
AFECTRL_HSTIAPWR | AFECTRL_INAMPPWR |
|
||||||
|
AFECTRL_EXTBUFPWR, bFALSE);
|
||||||
|
|
||||||
/* RCAL after */
|
sw.Dswitch = SWD_OPEN;
|
||||||
sw.Dswitch = ctx.rcal_sw_d;
|
sw.Pswitch = SWP_OPEN;
|
||||||
sw.Pswitch = ctx.rcal_sw_p;
|
sw.Nswitch = SWN_OPEN;
|
||||||
sw.Nswitch = ctx.rcal_sw_n;
|
sw.Tswitch = SWT_OPEN;
|
||||||
sw.Tswitch = ctx.rcal_sw_t;
|
AD5940_SWMatrixCfgS(&sw);
|
||||||
AD5940_SWMatrixCfgS(&sw);
|
|
||||||
AD5940_Delay10us(50);
|
|
||||||
|
|
||||||
fImpCar_Type rtia_after = measure_rtia(NULL);
|
/* ratiometric Z: (DftRcal / DftDut) * RCAL */
|
||||||
|
fImpCar_Type fr = { (float)rcal_hstia.Real, (float)rcal_hstia.Image };
|
||||||
|
fImpCar_Type fd = { (float)dut_hstia_raw.Real, (float)dut_hstia_raw.Image };
|
||||||
|
fImpCar_Type z = AD5940_ComplexDivFloat(&fr, &fd);
|
||||||
|
z.Real *= ctx.rcal_ohms;
|
||||||
|
z.Image *= ctx.rcal_ohms;
|
||||||
|
|
||||||
/* DUT reverse (DUT first, then RCAL) */
|
/* apply open-circuit compensation if available */
|
||||||
sw.Dswitch = ctx.dut_sw_d;
|
if (ocal.valid) {
|
||||||
sw.Pswitch = ctx.dut_sw_p;
|
for (uint32_t k = 0; k < ocal.n; k++) {
|
||||||
sw.Nswitch = ctx.dut_sw_n;
|
if (fabsf(ocal.freq[k] - freq_hz) < freq_hz * 0.01f) {
|
||||||
sw.Tswitch = ctx.dut_sw_t;
|
fImpCar_Type one = {1.0f, 0.0f};
|
||||||
AD5940_SWMatrixCfgS(&sw);
|
fImpCar_Type y_meas = AD5940_ComplexDivFloat(&one, &z);
|
||||||
AD5940_Delay10us(50);
|
fImpCar_Type y_corr = AD5940_ComplexSubFloat(&y_meas, &ocal.y[k]);
|
||||||
|
z = AD5940_ComplexDivFloat(&one, &y_corr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dft_measure_pair(ADCMUXP_HSTIA_P, ADCMUXN_HSTIA_N, &v_tia,
|
float mag = AD5940_ComplexMag(&z);
|
||||||
ctx.dut_mux_vp, ctx.dut_mux_vn, &v_sense);
|
float phase = AD5940_ComplexPhase(&z) * (float)(180.0 / M_PI);
|
||||||
v_tia.Real = -v_tia.Real;
|
float rtia_mag = AD5940_ComplexMag(&rtia);
|
||||||
v_tia.Image = -v_tia.Image;
|
|
||||||
|
|
||||||
iImpCar_Type v_tia_rev = v_tia;
|
out->freq_hz = freq_hz;
|
||||||
iImpCar_Type v_sense_rev = v_sense;
|
out->z_real = z.Real;
|
||||||
|
out->z_imag = z.Image;
|
||||||
|
out->mag_ohms = mag;
|
||||||
|
out->phase_deg = phase;
|
||||||
|
out->rtia_mag_before = rtia_mag;
|
||||||
|
out->rtia_mag_after = rtia_mag;
|
||||||
|
out->rev_mag = mag;
|
||||||
|
out->rev_phase = phase;
|
||||||
|
out->pct_err = 0.0f;
|
||||||
|
|
||||||
/* RCAL reverse */
|
return 0;
|
||||||
sw.Dswitch = ctx.rcal_sw_d;
|
|
||||||
sw.Pswitch = ctx.rcal_sw_p;
|
|
||||||
sw.Nswitch = ctx.rcal_sw_n;
|
|
||||||
sw.Tswitch = ctx.rcal_sw_t;
|
|
||||||
AD5940_SWMatrixCfgS(&sw);
|
|
||||||
AD5940_Delay10us(50);
|
|
||||||
|
|
||||||
fImpCar_Type rtia_rev = measure_rtia(NULL);
|
|
||||||
|
|
||||||
/* power down, open switches */
|
|
||||||
AD5940_AFECtrlS(AFECTRL_WG | AFECTRL_ADCPWR | AFECTRL_ADCCNV |
|
|
||||||
AFECTRL_DFT | AFECTRL_SINC2NOTCH | AFECTRL_HSDACPWR |
|
|
||||||
AFECTRL_HSTIAPWR | AFECTRL_INAMPPWR |
|
|
||||||
AFECTRL_EXTBUFPWR, bFALSE);
|
|
||||||
|
|
||||||
sw.Dswitch = SWD_OPEN;
|
|
||||||
sw.Pswitch = SWP_OPEN;
|
|
||||||
sw.Nswitch = SWN_OPEN;
|
|
||||||
sw.Tswitch = SWT_OPEN;
|
|
||||||
AD5940_SWMatrixCfgS(&sw);
|
|
||||||
|
|
||||||
/* forward Z using averaged RTIA bracket */
|
|
||||||
fImpCar_Type rtia_avg = {
|
|
||||||
.Real = (rtia_before.Real + rtia_after.Real) * 0.5f,
|
|
||||||
.Image = (rtia_before.Image + rtia_after.Image) * 0.5f,
|
|
||||||
};
|
|
||||||
fImpCar_Type fs_fwd = { (float)v_sense_fwd.Real, (float)v_sense_fwd.Image };
|
|
||||||
fImpCar_Type ft_fwd = { (float)v_tia_fwd.Real, (float)v_tia_fwd.Image };
|
|
||||||
fImpCar_Type num = AD5940_ComplexMulFloat(&fs_fwd, &rtia_avg);
|
|
||||||
fImpCar_Type z_fwd = AD5940_ComplexDivFloat(&num, &ft_fwd);
|
|
||||||
|
|
||||||
/* reverse Z using RTIA from RCAL measured after DUT */
|
|
||||||
fImpCar_Type fs_rev = { (float)v_sense_rev.Real, (float)v_sense_rev.Image };
|
|
||||||
fImpCar_Type ft_rev = { (float)v_tia_rev.Real, (float)v_tia_rev.Image };
|
|
||||||
num = AD5940_ComplexMulFloat(&fs_rev, &rtia_rev);
|
|
||||||
fImpCar_Type z_rev = AD5940_ComplexDivFloat(&num, &ft_rev);
|
|
||||||
(void)z_rev;
|
|
||||||
|
|
||||||
/* HSTIA-only ratiometric: Z = (DftRcal / DftDut) * RCAL */
|
|
||||||
fImpCar_Type fr = { (float)rcal_hstia.Real, (float)rcal_hstia.Image };
|
|
||||||
fImpCar_Type fd = { (float)dut_hstia_raw.Real, (float)dut_hstia_raw.Image };
|
|
||||||
fImpCar_Type z_ratio = AD5940_ComplexDivFloat(&fr, &fd);
|
|
||||||
z_ratio.Real *= ctx.rcal_ohms;
|
|
||||||
z_ratio.Image *= ctx.rcal_ohms;
|
|
||||||
|
|
||||||
/* apply open-circuit compensation if available */
|
|
||||||
if (ocal.valid) {
|
|
||||||
for (uint32_t k = 0; k < ocal.n; k++) {
|
|
||||||
if (fabsf(ocal.freq[k] - freq_hz) < freq_hz * 0.01f) {
|
|
||||||
fImpCar_Type one = {1.0f, 0.0f};
|
|
||||||
fImpCar_Type y_meas = AD5940_ComplexDivFloat(&one, &z_fwd);
|
|
||||||
fImpCar_Type y_corr = AD5940_ComplexSubFloat(&y_meas, &ocal.y[k]);
|
|
||||||
z_fwd = AD5940_ComplexDivFloat(&one, &y_corr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float mag_fwd = AD5940_ComplexMag(&z_fwd);
|
|
||||||
|
|
||||||
out->freq_hz = freq_hz;
|
|
||||||
out->z_real = z_fwd.Real;
|
|
||||||
out->z_imag = z_fwd.Image;
|
|
||||||
out->mag_ohms = mag_fwd;
|
|
||||||
out->phase_deg = AD5940_ComplexPhase(&z_fwd) * (float)(180.0 / M_PI);
|
|
||||||
out->rtia_mag_before = AD5940_ComplexMag(&rtia_before);
|
|
||||||
out->rtia_mag_after = AD5940_ComplexMag(&rtia_after);
|
|
||||||
out->rev_mag = AD5940_ComplexMag(&z_ratio);
|
|
||||||
out->rev_phase = AD5940_ComplexPhase(&z_ratio) * (float)(180.0 / M_PI);
|
|
||||||
out->pct_err = 0.0f;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int eis_sweep(EISPoint *out, uint32_t max_points, eis_point_cb_t cb)
|
int eis_sweep(EISPoint *out, uint32_t max_points, eis_point_cb_t cb)
|
||||||
|
|
@ -491,19 +448,16 @@ int eis_sweep(EISPoint *out, uint32_t max_points, eis_point_cb_t cb)
|
||||||
sweep.SweepLog = bTRUE;
|
sweep.SweepLog = bTRUE;
|
||||||
sweep.SweepIndex = 0;
|
sweep.SweepIndex = 0;
|
||||||
|
|
||||||
printf("\n%10s %12s %10s %12s %12s | %12s %10s %6s\n",
|
printf("\n%10s %12s %10s %12s %12s %6s\n",
|
||||||
"Freq(Hz)", "|Z|dual", "Ph_dual", "Re_dual", "Im_dual",
|
"Freq(Hz)", "|Z|", "Phase", "Re", "Im", "ms");
|
||||||
"|Z|ratio", "Ph_ratio", "ms");
|
printf("------------------------------------------------------------------\n");
|
||||||
printf("--------------------------------------------------------------------------"
|
|
||||||
"-------------------------\n");
|
|
||||||
|
|
||||||
uint32_t t0 = xTaskGetTickCount();
|
uint32_t t0 = xTaskGetTickCount();
|
||||||
eis_measure_point(ctx.cfg.freq_start_hz, &out[0]);
|
eis_measure_point(ctx.cfg.freq_start_hz, &out[0]);
|
||||||
uint32_t t1 = xTaskGetTickCount();
|
uint32_t t1 = xTaskGetTickCount();
|
||||||
printf("%10.1f %12.2f %10.2f %12.2f %12.2f | %12.2f %10.2f %6lu\n",
|
printf("%10.1f %12.2f %10.2f %12.2f %12.2f %6lu\n",
|
||||||
out[0].freq_hz, out[0].mag_ohms, out[0].phase_deg,
|
out[0].freq_hz, out[0].mag_ohms, out[0].phase_deg,
|
||||||
out[0].z_real, out[0].z_imag,
|
out[0].z_real, out[0].z_imag,
|
||||||
out[0].rev_mag, out[0].rev_phase,
|
|
||||||
(unsigned long)((t1 - t0) * portTICK_PERIOD_MS));
|
(unsigned long)((t1 - t0) * portTICK_PERIOD_MS));
|
||||||
if (cb) cb(0, &out[0]);
|
if (cb) cb(0, &out[0]);
|
||||||
|
|
||||||
|
|
@ -513,10 +467,9 @@ int eis_sweep(EISPoint *out, uint32_t max_points, eis_point_cb_t cb)
|
||||||
t0 = xTaskGetTickCount();
|
t0 = xTaskGetTickCount();
|
||||||
eis_measure_point(freq, &out[i]);
|
eis_measure_point(freq, &out[i]);
|
||||||
t1 = xTaskGetTickCount();
|
t1 = xTaskGetTickCount();
|
||||||
printf("%10.1f %12.2f %10.2f %12.2f %12.2f | %12.2f %10.2f %6lu\n",
|
printf("%10.1f %12.2f %10.2f %12.2f %12.2f %6lu\n",
|
||||||
out[i].freq_hz, out[i].mag_ohms, out[i].phase_deg,
|
out[i].freq_hz, out[i].mag_ohms, out[i].phase_deg,
|
||||||
out[i].z_real, out[i].z_imag,
|
out[i].z_real, out[i].z_imag,
|
||||||
out[i].rev_mag, out[i].rev_phase,
|
|
||||||
(unsigned long)((t1 - t0) * portTICK_PERIOD_MS));
|
(unsigned long)((t1 - t0) * portTICK_PERIOD_MS));
|
||||||
if (cb) cb((uint16_t)i, &out[i]);
|
if (cb) cb((uint16_t)i, &out[i]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
main/eis4.c
18
main/eis4.c
|
|
@ -193,6 +193,24 @@ void app_main(void)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case CMD_START_ORP: {
|
||||||
|
OrpConfig orp_cfg;
|
||||||
|
orp_cfg.stabilize_s = cmd.orp.stabilize_s;
|
||||||
|
orp_cfg.temp_c = temp_get();
|
||||||
|
printf("ORP: stabilize %.0f s, temp %.1f C\n",
|
||||||
|
orp_cfg.stabilize_s, orp_cfg.temp_c);
|
||||||
|
|
||||||
|
OrpResult orp_result;
|
||||||
|
echem_orp_read(&orp_cfg, &orp_result);
|
||||||
|
{
|
||||||
|
uint32_t ts_ms = (uint32_t)(esp_timer_get_time() / 1000);
|
||||||
|
measurement_counter++;
|
||||||
|
send_orp_result(orp_result.v_orp_mv, orp_result.temp_c,
|
||||||
|
ts_ms, measurement_counter);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case CMD_START_CLEAN:
|
case CMD_START_CLEAN:
|
||||||
printf("Clean: %.0f mV, %.0f s\n", cmd.clean.v_mv, cmd.clean.duration_s);
|
printf("Clean: %.0f mV, %.0f s\n", cmd.clean.v_mv, cmd.clean.duration_s);
|
||||||
echem_clean(cmd.clean.v_mv, cmd.clean.duration_s);
|
echem_clean(cmd.clean.v_mv, cmd.clean.duration_s);
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,22 @@ int send_ph_result(float v_ocp_mv, float ph, float temp_c,
|
||||||
return send_sysex(sx, p);
|
return send_sysex(sx, p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- outbound: ORP ---- */
|
||||||
|
|
||||||
|
int send_orp_result(float v_orp_mv, float temp_c,
|
||||||
|
uint32_t ts_ms, uint16_t meas_id)
|
||||||
|
{
|
||||||
|
uint8_t sx[24];
|
||||||
|
uint16_t p = 0;
|
||||||
|
sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_ORP_RESULT;
|
||||||
|
encode_float(v_orp_mv, &sx[p]); p += 5;
|
||||||
|
encode_float(temp_c, &sx[p]); p += 5;
|
||||||
|
encode_u32(ts_ms, &sx[p]); p += 5;
|
||||||
|
encode_u16(meas_id, &sx[p]); p += 3;
|
||||||
|
sx[p++] = 0xF7;
|
||||||
|
return send_sysex(sx, p);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- outbound: temperature ---- */
|
/* ---- outbound: temperature ---- */
|
||||||
|
|
||||||
int send_temp(float temp_c)
|
int send_temp(float temp_c)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
#define CMD_START_CL 0x23
|
#define CMD_START_CL 0x23
|
||||||
#define CMD_START_PH 0x24
|
#define CMD_START_PH 0x24
|
||||||
#define CMD_START_CLEAN 0x25
|
#define CMD_START_CLEAN 0x25
|
||||||
|
#define CMD_START_ORP 0x2A
|
||||||
#define CMD_OPEN_CAL 0x26
|
#define CMD_OPEN_CAL 0x26
|
||||||
#define CMD_CLEAR_OPEN_CAL 0x27
|
#define CMD_CLEAR_OPEN_CAL 0x27
|
||||||
#define CMD_SET_CELL_K 0x28
|
#define CMD_SET_CELL_K 0x28
|
||||||
|
|
@ -58,6 +59,7 @@
|
||||||
#define RSP_PH_RESULT 0x0F
|
#define RSP_PH_RESULT 0x0F
|
||||||
#define RSP_TEMP 0x10
|
#define RSP_TEMP 0x10
|
||||||
#define RSP_CELL_K 0x11
|
#define RSP_CELL_K 0x11
|
||||||
|
#define RSP_ORP_RESULT 0x12
|
||||||
#define RSP_REF_FRAME 0x20
|
#define RSP_REF_FRAME 0x20
|
||||||
#define RSP_REF_LP_RANGE 0x21
|
#define RSP_REF_LP_RANGE 0x21
|
||||||
#define RSP_REFS_DONE 0x22
|
#define RSP_REFS_DONE 0x22
|
||||||
|
|
@ -95,6 +97,7 @@ typedef struct {
|
||||||
struct { float v_hold, interval_ms, duration_s; uint8_t lp_rtia; } amp;
|
struct { float v_hold, interval_ms, duration_s; uint8_t lp_rtia; } amp;
|
||||||
struct { float v_cond, t_cond_ms, v_free, v_total, t_dep_ms, t_meas_ms; uint8_t lp_rtia; } cl;
|
struct { float v_cond, t_cond_ms, v_free, v_total, t_dep_ms, t_meas_ms; uint8_t lp_rtia; } cl;
|
||||||
struct { float stabilize_s; } ph;
|
struct { float stabilize_s; } ph;
|
||||||
|
struct { float stabilize_s; } orp;
|
||||||
struct { float v_mv; float duration_s; } clean;
|
struct { float v_mv; float duration_s; } clean;
|
||||||
float cell_k;
|
float cell_k;
|
||||||
float cl_factor;
|
float cl_factor;
|
||||||
|
|
@ -150,6 +153,10 @@ int send_cl_end(void);
|
||||||
int send_ph_result(float v_ocp_mv, float ph, float temp_c,
|
int send_ph_result(float v_ocp_mv, float ph, float temp_c,
|
||||||
uint32_t ts_ms, uint16_t meas_id);
|
uint32_t ts_ms, uint16_t meas_id);
|
||||||
|
|
||||||
|
/* outbound: ORP */
|
||||||
|
int send_orp_result(float v_orp_mv, float temp_c,
|
||||||
|
uint32_t ts_ms, uint16_t meas_id);
|
||||||
|
|
||||||
/* outbound: temperature */
|
/* outbound: temperature */
|
||||||
int send_temp(float temp_c);
|
int send_temp(float temp_c);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,10 @@ static void parse_udp_sysex(const uint8_t *data, uint16_t len)
|
||||||
if (len < 8) return;
|
if (len < 8) return;
|
||||||
cmd.ph.stabilize_s = decode_float(&data[3]);
|
cmd.ph.stabilize_s = decode_float(&data[3]);
|
||||||
break;
|
break;
|
||||||
|
case CMD_START_ORP:
|
||||||
|
if (len < 8) return;
|
||||||
|
cmd.orp.stabilize_s = decode_float(&data[3]);
|
||||||
|
break;
|
||||||
case CMD_START_CLEAN:
|
case CMD_START_CLEAN:
|
||||||
if (len < 13) return;
|
if (len < 13) return;
|
||||||
cmd.clean.v_mv = decode_float(&data[3]);
|
cmd.clean.v_mv = decode_float(&data[3]);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue