desktop: add pH cal protocol, Q/HQ peak detection, and state
This commit is contained in:
parent
5b051cfa20
commit
bdb72a9917
|
|
@ -100,6 +100,11 @@ pub enum Message {
|
||||||
CalComputeK,
|
CalComputeK,
|
||||||
ClCalKnownPpmChanged(String),
|
ClCalKnownPpmChanged(String),
|
||||||
ClSetFactor,
|
ClSetFactor,
|
||||||
|
/* pH calibration */
|
||||||
|
PhCalKnownChanged(String),
|
||||||
|
PhAddCalPoint,
|
||||||
|
PhClearCalPoints,
|
||||||
|
PhComputeAndSetCal,
|
||||||
/* Global */
|
/* Global */
|
||||||
PollTemp,
|
PollTemp,
|
||||||
NativeMenuTick,
|
NativeMenuTick,
|
||||||
|
|
@ -235,6 +240,10 @@ pub struct App {
|
||||||
cal_cell_constant: Option<f32>,
|
cal_cell_constant: Option<f32>,
|
||||||
cl_factor: Option<f32>,
|
cl_factor: Option<f32>,
|
||||||
cl_cal_known_ppm: String,
|
cl_cal_known_ppm: String,
|
||||||
|
ph_slope: Option<f32>,
|
||||||
|
ph_offset: Option<f32>,
|
||||||
|
ph_cal_points: Vec<(f32, f32)>,
|
||||||
|
ph_cal_known: String,
|
||||||
|
|
||||||
/* Global */
|
/* Global */
|
||||||
temp_c: f32,
|
temp_c: f32,
|
||||||
|
|
@ -469,6 +478,10 @@ impl App {
|
||||||
cal_cell_constant: None,
|
cal_cell_constant: None,
|
||||||
cl_factor: None,
|
cl_factor: None,
|
||||||
cl_cal_known_ppm: String::from("5"),
|
cl_cal_known_ppm: String::from("5"),
|
||||||
|
ph_slope: None,
|
||||||
|
ph_offset: None,
|
||||||
|
ph_cal_points: vec![],
|
||||||
|
ph_cal_known: String::from("7.00"),
|
||||||
|
|
||||||
temp_c: 25.0,
|
temp_c: 25.0,
|
||||||
conn_gen: 0,
|
conn_gen: 0,
|
||||||
|
|
@ -574,6 +587,7 @@ impl App {
|
||||||
self.send_cmd(&protocol::build_sysex_get_config());
|
self.send_cmd(&protocol::build_sysex_get_config());
|
||||||
self.send_cmd(&protocol::build_sysex_get_cell_k());
|
self.send_cmd(&protocol::build_sysex_get_cell_k());
|
||||||
self.send_cmd(&protocol::build_sysex_get_cl_factor());
|
self.send_cmd(&protocol::build_sysex_get_cl_factor());
|
||||||
|
self.send_cmd(&protocol::build_sysex_get_ph_cal());
|
||||||
}
|
}
|
||||||
Message::DeviceStatus(s) => {
|
Message::DeviceStatus(s) => {
|
||||||
if s.contains("Reconnecting") || s.contains("Connecting") {
|
if s.contains("Reconnecting") || s.contains("Connecting") {
|
||||||
|
|
@ -754,6 +768,11 @@ impl App {
|
||||||
self.cl_factor = Some(f);
|
self.cl_factor = Some(f);
|
||||||
self.status = format!("Device Cl factor: {:.6}", f);
|
self.status = format!("Device Cl factor: {:.6}", f);
|
||||||
}
|
}
|
||||||
|
EisMessage::PhCal { slope, offset } => {
|
||||||
|
self.ph_slope = Some(slope);
|
||||||
|
self.ph_offset = Some(offset);
|
||||||
|
self.status = format!("pH cal: slope={:.4} offset={:.4}", slope, offset);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Message::TabSelected(t) => {
|
Message::TabSelected(t) => {
|
||||||
if t == Tab::Browse {
|
if t == Tab::Browse {
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,31 @@ pub fn find_extrema(_v: &[f32], i_smooth: &[f32], min_prominence: f32) -> Vec<(u
|
||||||
candidates
|
candidates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detect Q/HQ redox peak in the -100 to +600 mV window.
|
||||||
|
/// Returns peak voltage in mV if found.
|
||||||
|
pub fn detect_qhq_peak(points: &[LsvPoint]) -> Option<f32> {
|
||||||
|
if points.len() < 5 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let i_vals: Vec<f32> = points.iter().map(|p| p.i_ua).collect();
|
||||||
|
let v_vals: Vec<f32> = points.iter().map(|p| p.v_mv).collect();
|
||||||
|
|
||||||
|
let window = 5.max(points.len() / 50);
|
||||||
|
let smoothed = smooth(&i_vals, window);
|
||||||
|
|
||||||
|
let i_min = smoothed.iter().copied().fold(f32::INFINITY, f32::min);
|
||||||
|
let i_max = smoothed.iter().copied().fold(f32::NEG_INFINITY, f32::max);
|
||||||
|
let prominence = (i_max - i_min) * 0.05;
|
||||||
|
|
||||||
|
let extrema = find_extrema(&v_vals, &smoothed, prominence);
|
||||||
|
|
||||||
|
extrema.iter()
|
||||||
|
.filter(|&&(idx, is_max)| is_max && v_vals[idx] >= -100.0 && v_vals[idx] <= 600.0)
|
||||||
|
.max_by(|a, b| smoothed[a.0].partial_cmp(&smoothed[b.0]).unwrap_or(std::cmp::Ordering::Equal))
|
||||||
|
.map(|&(idx, _)| v_vals[idx])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn detect_peaks(points: &[LsvPoint]) -> Vec<LsvPeak> {
|
pub fn detect_peaks(points: &[LsvPoint]) -> Vec<LsvPeak> {
|
||||||
if points.len() < 5 {
|
if points.len() < 5 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ pub const RSP_REFS_DONE: u8 = 0x22;
|
||||||
pub const RSP_CELL_K: u8 = 0x11;
|
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;
|
||||||
|
|
||||||
/* Cue → ESP32 */
|
/* Cue → ESP32 */
|
||||||
pub const CMD_SET_SWEEP: u8 = 0x10;
|
pub const CMD_SET_SWEEP: u8 = 0x10;
|
||||||
|
|
@ -47,6 +48,8 @@ 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;
|
||||||
pub const CMD_GET_CL_FACTOR: u8 = 0x34;
|
pub const CMD_GET_CL_FACTOR: u8 = 0x34;
|
||||||
|
pub const CMD_SET_PH_CAL: u8 = 0x35;
|
||||||
|
pub const CMD_GET_PH_CAL: u8 = 0x36;
|
||||||
pub const CMD_START_REFS: u8 = 0x30;
|
pub const CMD_START_REFS: u8 = 0x30;
|
||||||
pub const CMD_GET_REFS: u8 = 0x31;
|
pub const CMD_GET_REFS: u8 = 0x31;
|
||||||
pub const CMD_CLEAR_REFS: u8 = 0x32;
|
pub const CMD_CLEAR_REFS: u8 = 0x32;
|
||||||
|
|
@ -262,6 +265,7 @@ pub enum EisMessage {
|
||||||
RefStatus { has_refs: bool },
|
RefStatus { has_refs: bool },
|
||||||
CellK(f32),
|
CellK(f32),
|
||||||
ClFactor(f32),
|
ClFactor(f32),
|
||||||
|
PhCal { slope: f32, offset: f32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_u16(data: &[u8]) -> u16 {
|
fn decode_u16(data: &[u8]) -> u16 {
|
||||||
|
|
@ -421,6 +425,13 @@ pub fn parse_sysex(data: &[u8]) -> Option<EisMessage> {
|
||||||
let p = &data[2..];
|
let p = &data[2..];
|
||||||
Some(EisMessage::ClFactor(decode_float(&p[0..5])))
|
Some(EisMessage::ClFactor(decode_float(&p[0..5])))
|
||||||
}
|
}
|
||||||
|
RSP_PH_CAL if data.len() >= 12 => {
|
||||||
|
let p = &data[2..];
|
||||||
|
Some(EisMessage::PhCal {
|
||||||
|
slope: decode_float(&p[0..5]),
|
||||||
|
offset: decode_float(&p[5..10]),
|
||||||
|
})
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -546,3 +557,15 @@ pub fn build_sysex_set_cl_factor(f: f32) -> Vec<u8> {
|
||||||
pub fn build_sysex_get_cl_factor() -> Vec<u8> {
|
pub fn build_sysex_get_cl_factor() -> Vec<u8> {
|
||||||
vec![0xF0, SYSEX_MFR, CMD_GET_CL_FACTOR, 0xF7]
|
vec![0xF0, SYSEX_MFR, CMD_GET_CL_FACTOR, 0xF7]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_sysex_set_ph_cal(slope: f32, offset: f32) -> Vec<u8> {
|
||||||
|
let mut sx = vec![0xF0, SYSEX_MFR, CMD_SET_PH_CAL];
|
||||||
|
sx.extend_from_slice(&encode_float(slope));
|
||||||
|
sx.extend_from_slice(&encode_float(offset));
|
||||||
|
sx.push(0xF7);
|
||||||
|
sx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_sysex_get_ph_cal() -> Vec<u8> {
|
||||||
|
vec![0xF0, SYSEX_MFR, CMD_GET_PH_CAL, 0xF7]
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue