Merge branch 'chlorine-calibration-ui'
This commit is contained in:
commit
3c33c7806d
|
|
@ -95,6 +95,8 @@ final class AppState {
|
|||
var calTempC: String = "40"
|
||||
var calCellConstant: Double? = nil
|
||||
var calRs: Double? = nil
|
||||
var clFactor: Double? = nil
|
||||
var clCalKnownPpm: String = "5"
|
||||
|
||||
// Clean
|
||||
var cleanV: String = "1200"
|
||||
|
|
@ -256,6 +258,10 @@ final class AppState {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ let RSP_REF_FRAME: UInt8 = 0x20
|
|||
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
|
||||
|
||||
// Cue -> ESP32
|
||||
let CMD_SET_SWEEP: UInt8 = 0x10
|
||||
|
|
@ -46,6 +47,8 @@ let CMD_START_PH: UInt8 = 0x24
|
|||
let CMD_START_CLEAN: UInt8 = 0x25
|
||||
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_START_REFS: UInt8 = 0x30
|
||||
let CMD_GET_REFS: UInt8 = 0x31
|
||||
let CMD_CLEAR_REFS: UInt8 = 0x32
|
||||
|
|
@ -123,6 +126,7 @@ enum EisMessage {
|
|||
case refsDone
|
||||
case refStatus(hasRefs: Bool)
|
||||
case cellK(Float)
|
||||
case clFactor(Float)
|
||||
}
|
||||
|
||||
// MARK: - Response parser
|
||||
|
|
@ -254,6 +258,9 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
|
|||
case RSP_CELL_K where p.count >= 5:
|
||||
return .cellK(decodeFloat(p, at: 0))
|
||||
|
||||
case RSP_CL_FACTOR where p.count >= 5:
|
||||
return .clFactor(decodeFloat(p, at: 0))
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
@ -371,3 +378,14 @@ func buildSysexSetCellK(_ k: Float) -> [UInt8] {
|
|||
func buildSysexGetCellK() -> [UInt8] {
|
||||
[0xF0, sysexMfr, CMD_GET_CELL_K, 0xF7]
|
||||
}
|
||||
|
||||
func buildSysexSetClFactor(_ f: Float) -> [UInt8] {
|
||||
var sx: [UInt8] = [0xF0, sysexMfr, CMD_SET_CL_FACTOR]
|
||||
sx.append(contentsOf: encodeFloat(f))
|
||||
sx.append(0xF7)
|
||||
return sx
|
||||
}
|
||||
|
||||
func buildSysexGetClFactor() -> [UInt8] {
|
||||
[0xF0, sysexMfr, CMD_GET_CL_FACTOR, 0xF7]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ final class UDPManager: @unchecked Sendable {
|
|||
send(buildSysexGetTemp())
|
||||
send(buildSysexGetConfig())
|
||||
send(buildSysexGetCellK())
|
||||
send(buildSysexGetClFactor())
|
||||
startTimers()
|
||||
receiveLoop()
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ struct CalibrateView: View {
|
|||
inputSection
|
||||
resultsSection
|
||||
cellConstantSection
|
||||
chlorineCalSection
|
||||
}
|
||||
.navigationTitle("Calibrate")
|
||||
}
|
||||
|
|
@ -115,6 +116,48 @@ struct CalibrateView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Chlorine calibration
|
||||
|
||||
private var chlorineCalSection: some View {
|
||||
Section("Chlorine Calibration") {
|
||||
if let f = state.clFactor {
|
||||
Text(String(format: "Cl factor: %.6f ppm/\u{00B5}A", f))
|
||||
}
|
||||
if let r = state.clResult {
|
||||
Text(String(format: "Last free Cl peak: %.3f \u{00B5}A", r.iFreeUa))
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Known Cl ppm")
|
||||
Spacer()
|
||||
TextField("ppm", text: $state.clCalKnownPpm)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.frame(width: 80)
|
||||
#if os(iOS)
|
||||
.keyboardType(.decimalPad)
|
||||
#endif
|
||||
}
|
||||
|
||||
Button("Set Cl Factor") {
|
||||
guard let r = state.clResult else {
|
||||
state.status = "No chlorine measurement"
|
||||
return
|
||||
}
|
||||
let knownPpm = Double(state.clCalKnownPpm) ?? 0
|
||||
let peak = abs(Double(r.iFreeUa))
|
||||
guard peak > 0 else {
|
||||
state.status = "Peak current is zero"
|
||||
return
|
||||
}
|
||||
let factor = knownPpm / peak
|
||||
state.clFactor = factor
|
||||
state.send(buildSysexSetClFactor(Float(factor)))
|
||||
state.status = String(format: "Cl factor: %.6f ppm/\u{00B5}A", factor)
|
||||
}
|
||||
.disabled(state.clResult == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Calculations
|
||||
|
||||
private func saltGrams(volumeGal: Double, ppm: Double) -> Double {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,12 @@ struct ChlorineView: View {
|
|||
Text(String(format: "Combined: %.3f uA", r.iTotalUa - r.iFreeUa))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if let f = state.clFactor {
|
||||
let ppm = f * Double(abs(r.iFreeUa))
|
||||
Text(String(format: "Free Cl: %.2f ppm", ppm))
|
||||
.foregroundStyle(.cyan)
|
||||
}
|
||||
|
||||
if let (_, refR) = state.clRef {
|
||||
Divider().frame(height: 16)
|
||||
Text(String(format: "vs Ref: dFree=%.3f dTotal=%.3f",
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ pub enum Message {
|
|||
CalBleachChanged(String),
|
||||
CalTempChanged(String),
|
||||
CalComputeK,
|
||||
ClCalKnownPpmChanged(String),
|
||||
ClSetFactor,
|
||||
/* Global */
|
||||
PollTemp,
|
||||
NativeMenuTick,
|
||||
|
|
@ -231,6 +233,8 @@ pub struct App {
|
|||
cal_bleach_pct: String,
|
||||
cal_temp_c: String,
|
||||
cal_cell_constant: Option<f32>,
|
||||
cl_factor: Option<f32>,
|
||||
cl_cal_known_ppm: String,
|
||||
|
||||
/* Global */
|
||||
temp_c: f32,
|
||||
|
|
@ -463,6 +467,8 @@ impl App {
|
|||
cal_bleach_pct: "7.825".into(),
|
||||
cal_temp_c: "40".into(),
|
||||
cal_cell_constant: None,
|
||||
cl_factor: None,
|
||||
cl_cal_known_ppm: String::from("5"),
|
||||
|
||||
temp_c: 25.0,
|
||||
conn_gen: 0,
|
||||
|
|
@ -567,6 +573,7 @@ impl App {
|
|||
self.connected = true;
|
||||
self.send_cmd(&protocol::build_sysex_get_config());
|
||||
self.send_cmd(&protocol::build_sysex_get_cell_k());
|
||||
self.send_cmd(&protocol::build_sysex_get_cl_factor());
|
||||
}
|
||||
Message::DeviceStatus(s) => {
|
||||
if s.contains("Reconnecting") || s.contains("Connecting") {
|
||||
|
|
@ -743,6 +750,10 @@ impl App {
|
|||
self.cal_cell_constant = Some(k);
|
||||
self.status = format!("Device cell constant: {:.4} cm-1", k);
|
||||
}
|
||||
EisMessage::ClFactor(f) => {
|
||||
self.cl_factor = Some(f);
|
||||
self.status = format!("Device Cl factor: {:.6}", f);
|
||||
}
|
||||
},
|
||||
Message::TabSelected(t) => {
|
||||
if t == Tab::Browse {
|
||||
|
|
@ -968,6 +979,19 @@ impl App {
|
|||
self.status = "No valid EIS data for Rs extraction".into();
|
||||
}
|
||||
}
|
||||
Message::ClCalKnownPpmChanged(s) => { self.cl_cal_known_ppm = s; }
|
||||
Message::ClSetFactor => {
|
||||
let known_ppm = self.cl_cal_known_ppm.parse::<f32>().unwrap_or(0.0);
|
||||
if let Some(r) = &self.cl_result {
|
||||
let peak = r.i_free_ua.abs();
|
||||
if peak > 0.0 {
|
||||
let factor = known_ppm / peak;
|
||||
self.cl_factor = Some(factor);
|
||||
self.send_cmd(&protocol::build_sysex_set_cl_factor(factor));
|
||||
self.status = format!("Cl factor: {:.6} ppm/uA", factor);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Clean */
|
||||
Message::CleanVChanged(s) => self.clean_v = s,
|
||||
Message::CleanDurChanged(s) => self.clean_dur = s,
|
||||
|
|
@ -1574,6 +1598,10 @@ impl App {
|
|||
"Free: {:.3} uA | Total: {:.3} uA | Combined: {:.3} uA",
|
||||
r.i_free_ua, r.i_total_ua, r.i_total_ua - r.i_free_ua
|
||||
));
|
||||
if let (Some(f), Some(r)) = (self.cl_factor, &self.cl_result) {
|
||||
let ppm = f * r.i_free_ua.abs();
|
||||
result_parts.push(format!("Free Cl: {:.2} ppm", ppm));
|
||||
}
|
||||
if let Some((_, ref_r)) = &self.cl_ref {
|
||||
let df = r.i_free_ua - ref_r.i_free_ua;
|
||||
let dt = r.i_total_ua - ref_r.i_total_ua;
|
||||
|
|
@ -1681,6 +1709,28 @@ impl App {
|
|||
.on_press(Message::CalComputeK);
|
||||
results = results.push(compute_btn);
|
||||
|
||||
results = results.push(iced::widget::horizontal_rule(1));
|
||||
results = results.push(text("Chlorine Calibration").size(16));
|
||||
if let Some(f) = self.cl_factor {
|
||||
results = results.push(text(format!("Cl factor: {:.6} ppm/uA", f)).size(14));
|
||||
}
|
||||
if let Some(r) = &self.cl_result {
|
||||
results = results.push(text(format!("Last free Cl peak: {:.3} uA", r.i_free_ua)).size(14));
|
||||
}
|
||||
results = results.push(
|
||||
row![
|
||||
column![
|
||||
text("Known Cl ppm").size(12),
|
||||
text_input("5", &self.cl_cal_known_ppm)
|
||||
.on_input(Message::ClCalKnownPpmChanged).width(80),
|
||||
].spacing(2),
|
||||
button(text("Set Cl Factor").size(13))
|
||||
.style(style_action())
|
||||
.padding([6, 16])
|
||||
.on_press(Message::ClSetFactor),
|
||||
].spacing(10).align_y(iced::Alignment::End)
|
||||
);
|
||||
|
||||
row![
|
||||
container(inputs).width(Length::FillPortion(2)),
|
||||
iced::widget::vertical_rule(1),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ pub const RSP_REF_LP_RANGE: u8 = 0x21;
|
|||
pub const RSP_REFS_DONE: u8 = 0x22;
|
||||
pub const RSP_CELL_K: u8 = 0x11;
|
||||
pub const RSP_REF_STATUS: u8 = 0x23;
|
||||
pub const RSP_CL_FACTOR: u8 = 0x24;
|
||||
|
||||
/* Cue → ESP32 */
|
||||
pub const CMD_SET_SWEEP: u8 = 0x10;
|
||||
|
|
@ -44,6 +45,8 @@ pub const CMD_START_PH: u8 = 0x24;
|
|||
pub const CMD_START_CLEAN: u8 = 0x25;
|
||||
pub const CMD_SET_CELL_K: u8 = 0x28;
|
||||
pub const CMD_GET_CELL_K: u8 = 0x29;
|
||||
pub const CMD_SET_CL_FACTOR: u8 = 0x33;
|
||||
pub const CMD_GET_CL_FACTOR: u8 = 0x34;
|
||||
pub const CMD_START_REFS: u8 = 0x30;
|
||||
pub const CMD_GET_REFS: u8 = 0x31;
|
||||
pub const CMD_CLEAR_REFS: u8 = 0x32;
|
||||
|
|
@ -258,6 +261,7 @@ pub enum EisMessage {
|
|||
RefsDone,
|
||||
RefStatus { has_refs: bool },
|
||||
CellK(f32),
|
||||
ClFactor(f32),
|
||||
}
|
||||
|
||||
fn decode_u16(data: &[u8]) -> u16 {
|
||||
|
|
@ -413,6 +417,10 @@ pub fn parse_sysex(data: &[u8]) -> Option<EisMessage> {
|
|||
let p = &data[2..];
|
||||
Some(EisMessage::CellK(decode_float(&p[0..5])))
|
||||
}
|
||||
RSP_CL_FACTOR if data.len() >= 7 => {
|
||||
let p = &data[2..];
|
||||
Some(EisMessage::ClFactor(decode_float(&p[0..5])))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -527,3 +535,14 @@ pub fn build_sysex_set_cell_k(k: f32) -> Vec<u8> {
|
|||
pub fn build_sysex_get_cell_k() -> Vec<u8> {
|
||||
vec![0xF0, SYSEX_MFR, CMD_GET_CELL_K, 0xF7]
|
||||
}
|
||||
|
||||
pub fn build_sysex_set_cl_factor(f: f32) -> Vec<u8> {
|
||||
let mut sx = vec![0xF0, SYSEX_MFR, CMD_SET_CL_FACTOR];
|
||||
sx.extend_from_slice(&encode_float(f));
|
||||
sx.push(0xF7);
|
||||
sx
|
||||
}
|
||||
|
||||
pub fn build_sysex_get_cl_factor() -> Vec<u8> {
|
||||
vec![0xF0, SYSEX_MFR, CMD_GET_CL_FACTOR, 0xF7]
|
||||
}
|
||||
|
|
|
|||
29
main/eis.c
29
main/eis.c
|
|
@ -25,6 +25,7 @@ static struct {
|
|||
|
||||
/* cell constant K (cm⁻¹), cached from NVS */
|
||||
static float cell_k_cached;
|
||||
static float cl_factor_cached;
|
||||
|
||||
/* open-circuit calibration data */
|
||||
static struct {
|
||||
|
|
@ -593,7 +594,8 @@ int eis_has_open_cal(void)
|
|||
return ocal.valid;
|
||||
}
|
||||
|
||||
#define NVS_CELLK_KEY "cell_k"
|
||||
#define NVS_CELLK_KEY "cell_k"
|
||||
#define NVS_CLFACTOR_KEY "cl_factor"
|
||||
|
||||
void eis_set_cell_k(float k)
|
||||
{
|
||||
|
|
@ -619,3 +621,28 @@ void eis_load_cell_k(void)
|
|||
cell_k_cached = 0.0f;
|
||||
nvs_close(h);
|
||||
}
|
||||
|
||||
void eis_set_cl_factor(float f)
|
||||
{
|
||||
cl_factor_cached = f;
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_OCAL_NS, NVS_READWRITE, &h) != ESP_OK) return;
|
||||
nvs_set_blob(h, NVS_CLFACTOR_KEY, &f, sizeof(f));
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
}
|
||||
|
||||
float eis_get_cl_factor(void)
|
||||
{
|
||||
return cl_factor_cached;
|
||||
}
|
||||
|
||||
void eis_load_cl_factor(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_OCAL_NS, NVS_READONLY, &h) != ESP_OK) return;
|
||||
size_t len = sizeof(cl_factor_cached);
|
||||
if (nvs_get_blob(h, NVS_CLFACTOR_KEY, &cl_factor_cached, &len) != ESP_OK || len != sizeof(cl_factor_cached))
|
||||
cl_factor_cached = 0.0f;
|
||||
nvs_close(h);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,4 +67,8 @@ void eis_set_cell_k(float k);
|
|||
float eis_get_cell_k(void);
|
||||
void eis_load_cell_k(void);
|
||||
|
||||
void eis_set_cl_factor(float f);
|
||||
float eis_get_cl_factor(void);
|
||||
void eis_load_cl_factor(void);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
11
main/eis4.c
11
main/eis4.c
|
|
@ -56,6 +56,7 @@ void app_main(void)
|
|||
eis_default_config(&cfg);
|
||||
eis_load_open_cal();
|
||||
eis_load_cell_k();
|
||||
eis_load_cl_factor();
|
||||
temp_init();
|
||||
|
||||
esp_netif_init();
|
||||
|
|
@ -214,6 +215,16 @@ void app_main(void)
|
|||
send_cell_k(eis_get_cell_k());
|
||||
break;
|
||||
|
||||
case CMD_SET_CL_FACTOR:
|
||||
eis_set_cl_factor(cmd.cl_factor);
|
||||
send_cl_factor(cmd.cl_factor);
|
||||
printf("Cl factor set: %.6f\n", cmd.cl_factor);
|
||||
break;
|
||||
|
||||
case CMD_GET_CL_FACTOR:
|
||||
send_cl_factor(eis_get_cl_factor());
|
||||
break;
|
||||
|
||||
case CMD_START_CL: {
|
||||
ClConfig cl_cfg;
|
||||
cl_cfg.v_cond = cmd.cl.v_cond;
|
||||
|
|
|
|||
|
|
@ -341,6 +341,18 @@ int send_cell_k(float k)
|
|||
return send_sysex(sx, p);
|
||||
}
|
||||
|
||||
/* ---- outbound: chlorine factor ---- */
|
||||
|
||||
int send_cl_factor(float f)
|
||||
{
|
||||
uint8_t sx[12];
|
||||
uint16_t p = 0;
|
||||
sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_CL_FACTOR;
|
||||
encode_float(f, &sx[p]); p += 5;
|
||||
sx[p++] = 0xF7;
|
||||
return send_sysex(sx, p);
|
||||
}
|
||||
|
||||
/* ---- outbound: reference collection ---- */
|
||||
|
||||
int send_ref_frame(uint8_t mode, uint8_t rtia_idx)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@
|
|||
#define CMD_START_REFS 0x30
|
||||
#define CMD_GET_REFS 0x31
|
||||
#define CMD_CLEAR_REFS 0x32
|
||||
#define CMD_SET_CL_FACTOR 0x33
|
||||
#define CMD_GET_CL_FACTOR 0x34
|
||||
|
||||
/* Session sync commands (0x4x) */
|
||||
#define CMD_SESSION_CREATE 0x40
|
||||
|
|
@ -56,6 +58,7 @@
|
|||
#define RSP_REF_LP_RANGE 0x21
|
||||
#define RSP_REFS_DONE 0x22
|
||||
#define RSP_REF_STATUS 0x23
|
||||
#define RSP_CL_FACTOR 0x24
|
||||
|
||||
/* Session sync responses (0x4x) */
|
||||
#define RSP_SESSION_CREATED 0x40
|
||||
|
|
@ -81,6 +84,7 @@ typedef struct {
|
|||
struct { float stabilize_s; } ph;
|
||||
struct { float v_mv; float duration_s; } clean;
|
||||
float cell_k;
|
||||
float cl_factor;
|
||||
struct { uint8_t name_len; char name[MAX_SESSION_NAME]; } session_create;
|
||||
struct { uint8_t id; } session_switch;
|
||||
struct { uint8_t id; uint8_t name_len; char name[MAX_SESSION_NAME]; } session_rename;
|
||||
|
|
@ -132,6 +136,9 @@ int send_temp(float temp_c);
|
|||
/* outbound: cell constant */
|
||||
int send_cell_k(float k);
|
||||
|
||||
/* outbound: chlorine factor */
|
||||
int send_cl_factor(float f);
|
||||
|
||||
/* outbound: reference collection */
|
||||
int send_ref_frame(uint8_t mode, uint8_t rtia_idx);
|
||||
int send_ref_lp_range(uint8_t mode, uint8_t low_idx, uint8_t high_idx);
|
||||
|
|
|
|||
|
|
@ -166,6 +166,10 @@ static void parse_udp_sysex(const uint8_t *data, uint16_t len)
|
|||
if (len < 8) return;
|
||||
cmd.cell_k = decode_float(&data[3]);
|
||||
break;
|
||||
case CMD_SET_CL_FACTOR:
|
||||
if (len < 8) return;
|
||||
cmd.cl_factor = decode_float(&data[3]);
|
||||
break;
|
||||
case CMD_SESSION_CREATE:
|
||||
if (len < 5) return;
|
||||
cmd.session_create.name_len = data[3] & 0x7F;
|
||||
|
|
@ -192,6 +196,7 @@ static void parse_udp_sysex(const uint8_t *data, uint16_t len)
|
|||
case CMD_STOP_AMP:
|
||||
case CMD_GET_TEMP:
|
||||
case CMD_GET_CELL_K:
|
||||
case CMD_GET_CL_FACTOR:
|
||||
case CMD_START_REFS:
|
||||
case CMD_GET_REFS:
|
||||
case CMD_CLEAR_REFS:
|
||||
|
|
|
|||
Loading…
Reference in New Issue