/// EIS4 SysEx Protocol — manufacturer ID 0x7D (non-commercial) use serde::{Serialize, Deserialize}; pub const SYSEX_MFR: u8 = 0x7D; /* ESP32 → Cue */ pub const RSP_SWEEP_START: u8 = 0x01; pub const RSP_DATA_POINT: u8 = 0x02; pub const RSP_SWEEP_END: u8 = 0x03; pub const RSP_CONFIG: u8 = 0x04; pub const RSP_LSV_START: u8 = 0x05; pub const RSP_LSV_POINT: u8 = 0x06; pub const RSP_LSV_END: u8 = 0x07; pub const RSP_AMP_START: u8 = 0x08; pub const RSP_AMP_POINT: u8 = 0x09; pub const RSP_AMP_END: u8 = 0x0A; pub const RSP_CL_START: u8 = 0x0B; pub const RSP_CL_POINT: u8 = 0x0C; pub const RSP_CL_RESULT: u8 = 0x0D; pub const RSP_CL_END: u8 = 0x0E; pub const RSP_PH_RESULT: u8 = 0x0F; pub const RSP_TEMP: u8 = 0x10; pub const RSP_REF_FRAME: u8 = 0x20; 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; pub const RSP_PH_CAL: u8 = 0x25; pub const RSP_KEEPALIVE: u8 = 0x50; /* Cue → ESP32 */ pub const CMD_SET_SWEEP: u8 = 0x10; pub const CMD_SET_RTIA: u8 = 0x11; pub const CMD_SET_RCAL: u8 = 0x12; pub const CMD_START_SWEEP: u8 = 0x13; pub const CMD_GET_CONFIG: u8 = 0x14; pub const CMD_SET_ELECTRODE: u8 = 0x15; pub const CMD_START_LSV: u8 = 0x20; pub const CMD_START_AMP: u8 = 0x21; pub const CMD_STOP_AMP: u8 = 0x22; pub const CMD_GET_TEMP: u8 = 0x17; pub const CMD_START_CL: u8 = 0x23; 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_SET_PH_CAL: u8 = 0x35; pub const CMD_GET_PH_CAL: u8 = 0x36; pub const CMD_START_REFS: u8 = 0x30; pub const CMD_GET_REFS: u8 = 0x31; pub const CMD_CLEAR_REFS: u8 = 0x32; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Rtia { R200, R1K, R5K, R10K, R20K, R40K, R80K, R160K, ExtDe0, } impl Rtia { pub fn from_byte(b: u8) -> Option { Some(match b { 0 => Self::R200, 1 => Self::R1K, 2 => Self::R5K, 3 => Self::R10K, 4 => Self::R20K, 5 => Self::R40K, 6 => Self::R80K, 7 => Self::R160K, 8 => Self::ExtDe0, _ => return None, }) } pub fn as_byte(self) -> u8 { self as u8 } pub fn label(self) -> &'static str { match self { Self::R200 => "200Ω", Self::R1K => "1kΩ", Self::R5K => "5kΩ", Self::R10K => "10kΩ", Self::R20K => "20kΩ", Self::R40K => "40kΩ", Self::R80K => "80kΩ", Self::R160K => "160kΩ", Self::ExtDe0 => "Ext 3kΩ (DE0)", } } pub const ALL: &[Self] = &[ Self::R200, Self::R1K, Self::R5K, Self::R10K, Self::R20K, Self::R40K, Self::R80K, Self::R160K, Self::ExtDe0, ]; } impl std::fmt::Display for Rtia { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.label()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Rcal { R200, R3K, } impl Rcal { pub fn from_byte(b: u8) -> Option { Some(match b { 0 => Self::R200, 1 => Self::R3K, _ => return None }) } pub fn as_byte(self) -> u8 { self as u8 } pub fn label(self) -> &'static str { match self { Self::R200 => "200Ω (RCAL0↔RCAL1)", Self::R3K => "3kΩ (RCAL0↔AIN0)" } } pub const ALL: &[Self] = &[Self::R200, Self::R3K]; } impl std::fmt::Display for Rcal { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.label()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Electrode { FourWire, ThreeWire, } impl Electrode { pub fn from_byte(b: u8) -> Option { Some(match b { 0 => Self::FourWire, 1 => Self::ThreeWire, _ => return None }) } pub fn as_byte(self) -> u8 { self as u8 } pub fn label(self) -> &'static str { match self { Self::FourWire => "4-wire (AIN)", Self::ThreeWire => "3-wire (CE0/RE0/SE0)", } } pub const ALL: &[Self] = &[Self::FourWire, Self::ThreeWire]; } impl std::fmt::Display for Electrode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.label()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LpRtia { R200, R1K, R2K, R3K, R4K, R6K, R8K, R10K, R12K, R16K, R20K, R24K, R30K, R32K, R40K, R48K, R64K, R85K, R96K, R100K, R120K, R128K, R160K, R196K, R256K, R512K, } impl LpRtia { #[allow(dead_code)] pub fn from_byte(b: u8) -> Option { Some(match b { 0 => Self::R200, 1 => Self::R1K, 2 => Self::R2K, 3 => Self::R3K, 4 => Self::R4K, 5 => Self::R6K, 6 => Self::R8K, 7 => Self::R10K, 8 => Self::R12K, 9 => Self::R16K, 10 => Self::R20K, 11 => Self::R24K, 12 => Self::R30K, 13 => Self::R32K, 14 => Self::R40K, 15 => Self::R48K, 16 => Self::R64K, 17 => Self::R85K, 18 => Self::R96K, 19 => Self::R100K, 20 => Self::R120K, 21 => Self::R128K, 22 => Self::R160K, 23 => Self::R196K, 24 => Self::R256K, 25 => Self::R512K, _ => return None, }) } pub fn as_byte(self) -> u8 { self as u8 } pub fn label(self) -> &'static str { match self { Self::R200 => "200Ω", Self::R1K => "1kΩ", Self::R2K => "2kΩ", Self::R3K => "3kΩ", Self::R4K => "4kΩ", Self::R6K => "6kΩ", Self::R8K => "8kΩ", Self::R10K => "10kΩ", Self::R12K => "12kΩ", Self::R16K => "16kΩ", Self::R20K => "20kΩ", Self::R24K => "24kΩ", Self::R30K => "30kΩ", Self::R32K => "32kΩ", Self::R40K => "40kΩ", Self::R48K => "48kΩ", Self::R64K => "64kΩ", Self::R85K => "85kΩ", Self::R96K => "96kΩ", Self::R100K => "100kΩ", Self::R120K => "120kΩ", Self::R128K => "128kΩ", Self::R160K => "160kΩ", Self::R196K => "196kΩ", Self::R256K => "256kΩ", Self::R512K => "512kΩ", } } pub const ALL: &[Self] = &[ Self::R200, Self::R1K, Self::R2K, Self::R3K, Self::R4K, Self::R6K, Self::R8K, Self::R10K, Self::R12K, Self::R16K, Self::R20K, Self::R24K, Self::R30K, Self::R32K, Self::R40K, Self::R48K, Self::R64K, Self::R85K, Self::R96K, Self::R100K, Self::R120K, Self::R128K, Self::R160K, Self::R196K, Self::R256K, Self::R512K, ]; } impl std::fmt::Display for LpRtia { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.label()) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EisPoint { pub freq_hz: f32, pub mag_ohms: f32, pub phase_deg: f32, pub z_real: f32, pub z_imag: f32, pub rtia_mag_before: f32, pub rtia_mag_after: f32, pub rev_mag: f32, pub rev_phase: f32, pub pct_err: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LsvPoint { pub v_mv: f32, pub i_ua: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AmpPoint { pub t_ms: f32, pub i_ua: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClPoint { pub t_ms: f32, pub i_ua: f32, pub phase: u8, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClResult { pub i_free_ua: f32, pub i_total_ua: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PhResult { pub v_ocp_mv: f32, pub ph: f32, pub temp_c: f32, } #[derive(Debug, Clone)] pub struct EisConfig { pub freq_start: f32, pub freq_stop: f32, pub ppd: u16, pub rtia: Rtia, pub rcal: Rcal, pub electrode: Electrode, } #[derive(Debug, Clone)] pub enum EisMessage { SweepStart { num_points: u16, freq_start: f32, freq_stop: f32, esp_timestamp: Option, meas_id: Option }, DataPoint { _index: u16, point: EisPoint }, SweepEnd, Config(EisConfig), LsvStart { num_points: u16, v_start: f32, v_stop: f32, esp_timestamp: Option, meas_id: Option }, LsvPoint { _index: u16, point: LsvPoint }, LsvEnd, AmpStart { v_hold: f32, esp_timestamp: Option, meas_id: Option }, AmpPoint { _index: u16, point: AmpPoint }, AmpEnd, ClStart { num_points: u16, esp_timestamp: Option, meas_id: Option }, ClPoint { _index: u16, point: ClPoint }, ClResult(ClResult), ClEnd, PhResult(PhResult, Option, Option), Temperature(f32), RefFrame { mode: u8, rtia_idx: u8 }, RefLpRange { mode: u8, low_idx: u8, high_idx: u8 }, RefsDone, RefStatus { has_refs: bool }, CellK(f32), ClFactor(f32), PhCal { slope: f32, offset: f32 }, Keepalive, } fn decode_u16(data: &[u8]) -> u16 { let b0 = data[1] | ((data[0] & 1) << 7); let b1 = data[2] | ((data[0] & 2) << 6); u16::from_le_bytes([b0, b1]) } fn decode_float(data: &[u8]) -> f32 { let m = data[0]; let b0 = data[1] | ((m & 1) << 7); let b1 = data[2] | ((m & 2) << 6); let b2 = data[3] | ((m & 4) << 5); let b3 = data[4] | ((m & 8) << 4); f32::from_le_bytes([b0, b1, b2, b3]) } fn decode_u32(data: &[u8]) -> u32 { let b = [ data[1] | ((data[0] & 1) << 7), data[2] | ((data[0] & 2) << 6), data[3] | ((data[0] & 4) << 5), data[4] | ((data[0] & 8) << 4), ]; u32::from_le_bytes(b) } fn encode_float(val: f32) -> [u8; 5] { let p = val.to_le_bytes(); [ ((p[0] >> 7) & 1) | ((p[1] >> 6) & 2) | ((p[2] >> 5) & 4) | ((p[3] >> 4) & 8), p[0] & 0x7F, p[1] & 0x7F, p[2] & 0x7F, p[3] & 0x7F, ] } fn encode_u16(val: u16) -> [u8; 3] { let p = val.to_le_bytes(); [((p[0] >> 7) & 1) | ((p[1] >> 6) & 2), p[0] & 0x7F, p[1] & 0x7F] } pub fn parse_sysex(data: &[u8]) -> Option { if data.len() < 2 || data[0] != SYSEX_MFR { return None; } match data[1] { RSP_SWEEP_START if data.len() >= 15 => { let p = &data[2..]; let (ts, mid) = if p.len() >= 21 { (Some(decode_u32(&p[13..18])), Some(decode_u16(&p[18..21]))) } else { (None, None) }; Some(EisMessage::SweepStart { num_points: decode_u16(&p[0..3]), freq_start: decode_float(&p[3..8]), freq_stop: decode_float(&p[8..13]), esp_timestamp: ts, meas_id: mid, }) } RSP_DATA_POINT if data.len() >= 30 => { let p = &data[2..]; let ext = p.len() >= 53; Some(EisMessage::DataPoint { _index: decode_u16(&p[0..3]), point: EisPoint { freq_hz: decode_float(&p[3..8]), mag_ohms: decode_float(&p[8..13]), phase_deg: decode_float(&p[13..18]), z_real: decode_float(&p[18..23]), z_imag: decode_float(&p[23..28]), rtia_mag_before: if ext { decode_float(&p[28..33]) } else { 0.0 }, rtia_mag_after: if ext { decode_float(&p[33..38]) } else { 0.0 }, rev_mag: if ext { decode_float(&p[38..43]) } else { 0.0 }, rev_phase: if ext { decode_float(&p[43..48]) } else { 0.0 }, pct_err: if ext { decode_float(&p[48..53]) } else { 0.0 }, }, }) } RSP_SWEEP_END => Some(EisMessage::SweepEnd), RSP_CONFIG if data.len() >= 18 => { let p = &data[2..]; Some(EisMessage::Config(EisConfig { freq_start: decode_float(&p[0..5]), freq_stop: decode_float(&p[5..10]), ppd: decode_u16(&p[10..13]), rtia: Rtia::from_byte(p[13]).unwrap_or(Rtia::R5K), rcal: Rcal::from_byte(p[14]).unwrap_or(Rcal::R3K), electrode: Electrode::from_byte(p[15]).unwrap_or(Electrode::FourWire), })) } RSP_LSV_START if data.len() >= 15 => { let p = &data[2..]; let (ts, mid) = if p.len() >= 21 { (Some(decode_u32(&p[13..18])), Some(decode_u16(&p[18..21]))) } else { (None, None) }; Some(EisMessage::LsvStart { num_points: decode_u16(&p[0..3]), v_start: decode_float(&p[3..8]), v_stop: decode_float(&p[8..13]), esp_timestamp: ts, meas_id: mid, }) } RSP_LSV_POINT if data.len() >= 15 => { let p = &data[2..]; Some(EisMessage::LsvPoint { _index: decode_u16(&p[0..3]), point: LsvPoint { v_mv: decode_float(&p[3..8]), i_ua: decode_float(&p[8..13]), }, }) } RSP_LSV_END => Some(EisMessage::LsvEnd), RSP_AMP_START if data.len() >= 7 => { let p = &data[2..]; let (ts, mid) = if p.len() >= 13 { (Some(decode_u32(&p[5..10])), Some(decode_u16(&p[10..13]))) } else { (None, None) }; Some(EisMessage::AmpStart { v_hold: decode_float(&p[0..5]), esp_timestamp: ts, meas_id: mid }) } RSP_AMP_POINT if data.len() >= 15 => { let p = &data[2..]; Some(EisMessage::AmpPoint { _index: decode_u16(&p[0..3]), point: AmpPoint { t_ms: decode_float(&p[3..8]), i_ua: decode_float(&p[8..13]), }, }) } RSP_AMP_END => Some(EisMessage::AmpEnd), RSP_CL_START if data.len() >= 5 => { let p = &data[2..]; let (ts, mid) = if p.len() >= 11 { (Some(decode_u32(&p[3..8])), Some(decode_u16(&p[8..11]))) } else { (None, None) }; Some(EisMessage::ClStart { num_points: decode_u16(&p[0..3]), esp_timestamp: ts, meas_id: mid }) } RSP_CL_POINT if data.len() >= 16 => { let p = &data[2..]; Some(EisMessage::ClPoint { _index: decode_u16(&p[0..3]), point: ClPoint { t_ms: decode_float(&p[3..8]), i_ua: decode_float(&p[8..13]), phase: p[13], }, }) } RSP_CL_RESULT if data.len() >= 12 => { let p = &data[2..]; Some(EisMessage::ClResult(ClResult { i_free_ua: decode_float(&p[0..5]), i_total_ua: decode_float(&p[5..10]), })) } RSP_CL_END => Some(EisMessage::ClEnd), RSP_TEMP if data.len() >= 7 => { let p = &data[2..]; Some(EisMessage::Temperature(decode_float(&p[0..5]))) } RSP_PH_RESULT if data.len() >= 17 => { let p = &data[2..]; let (ts, mid) = if p.len() >= 23 { (Some(decode_u32(&p[15..20])), Some(decode_u16(&p[20..23]))) } else { (None, None) }; Some(EisMessage::PhResult(PhResult { v_ocp_mv: decode_float(&p[0..5]), ph: decode_float(&p[5..10]), temp_c: decode_float(&p[10..15]), }, ts, mid)) } RSP_REF_FRAME if data.len() >= 4 => { Some(EisMessage::RefFrame { mode: data[2], rtia_idx: data[3] }) } RSP_REF_LP_RANGE if data.len() >= 5 => { Some(EisMessage::RefLpRange { mode: data[2], low_idx: data[3], high_idx: data[4] }) } RSP_REFS_DONE => Some(EisMessage::RefsDone), RSP_REF_STATUS if data.len() >= 3 => { Some(EisMessage::RefStatus { has_refs: data[2] != 0 }) } RSP_CELL_K if data.len() >= 7 => { 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]))) } 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]), }) } RSP_KEEPALIVE => Some(EisMessage::Keepalive), _ => None, } } pub fn build_sysex_set_sweep(freq_start: f32, freq_stop: f32, ppd: u16) -> Vec { let mut sx = vec![0xF0, SYSEX_MFR, CMD_SET_SWEEP]; sx.extend_from_slice(&encode_float(freq_start)); sx.extend_from_slice(&encode_float(freq_stop)); sx.extend_from_slice(&encode_u16(ppd)); sx.push(0xF7); sx } pub fn build_sysex_set_rtia(rtia: Rtia) -> Vec { vec![0xF0, SYSEX_MFR, CMD_SET_RTIA, rtia.as_byte(), 0xF7] } pub fn build_sysex_set_rcal(rcal: Rcal) -> Vec { vec![0xF0, SYSEX_MFR, CMD_SET_RCAL, rcal.as_byte(), 0xF7] } pub fn build_sysex_set_electrode(e: Electrode) -> Vec { vec![0xF0, SYSEX_MFR, CMD_SET_ELECTRODE, e.as_byte(), 0xF7] } pub fn build_sysex_start_sweep() -> Vec { vec![0xF0, SYSEX_MFR, CMD_START_SWEEP, 0xF7] } pub fn build_sysex_get_config() -> Vec { vec![0xF0, SYSEX_MFR, CMD_GET_CONFIG, 0xF7] } pub fn build_sysex_start_lsv(v_start: f32, v_stop: f32, scan_rate: f32, lp_rtia: LpRtia, num_points: u16) -> Vec { let mut sx = vec![0xF0, SYSEX_MFR, CMD_START_LSV]; sx.extend_from_slice(&encode_float(v_start)); sx.extend_from_slice(&encode_float(v_stop)); sx.extend_from_slice(&encode_float(scan_rate)); sx.push(lp_rtia.as_byte()); sx.extend_from_slice(&encode_u16(num_points)); sx.push(0xF7); sx } pub fn build_sysex_start_amp(v_hold: f32, interval_ms: f32, duration_s: f32, lp_rtia: LpRtia) -> Vec { let mut sx = vec![0xF0, SYSEX_MFR, CMD_START_AMP]; sx.extend_from_slice(&encode_float(v_hold)); sx.extend_from_slice(&encode_float(interval_ms)); sx.extend_from_slice(&encode_float(duration_s)); sx.push(lp_rtia.as_byte()); sx.push(0xF7); sx } pub fn build_sysex_stop_amp() -> Vec { vec![0xF0, SYSEX_MFR, CMD_STOP_AMP, 0xF7] } pub fn build_sysex_start_cl( v_cond: f32, t_cond_ms: f32, v_free: f32, v_total: f32, t_dep_ms: f32, t_meas_ms: f32, lp_rtia: LpRtia, ) -> Vec { let mut sx = vec![0xF0, SYSEX_MFR, CMD_START_CL]; sx.extend_from_slice(&encode_float(v_cond)); sx.extend_from_slice(&encode_float(t_cond_ms)); sx.extend_from_slice(&encode_float(v_free)); sx.extend_from_slice(&encode_float(v_total)); sx.extend_from_slice(&encode_float(t_dep_ms)); sx.extend_from_slice(&encode_float(t_meas_ms)); sx.push(lp_rtia.as_byte()); sx.push(0xF7); sx } pub fn build_sysex_get_temp() -> Vec { vec![0xF0, SYSEX_MFR, CMD_GET_TEMP, 0xF7] } pub fn build_sysex_start_ph(stabilize_s: f32) -> Vec { let mut sx = vec![0xF0, SYSEX_MFR, CMD_START_PH]; 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 { let mut sx = vec![0xF0, SYSEX_MFR, CMD_START_CLEAN]; sx.extend_from_slice(&encode_float(v_mv)); sx.extend_from_slice(&encode_float(duration_s)); sx.push(0xF7); sx } pub fn build_sysex_start_refs() -> Vec { vec![0xF0, SYSEX_MFR, CMD_START_REFS, 0xF7] } pub fn build_sysex_get_refs() -> Vec { vec![0xF0, SYSEX_MFR, CMD_GET_REFS, 0xF7] } pub fn build_sysex_clear_refs() -> Vec { vec![0xF0, SYSEX_MFR, CMD_CLEAR_REFS, 0xF7] } pub fn build_sysex_set_cell_k(k: f32) -> Vec { let mut sx = vec![0xF0, SYSEX_MFR, CMD_SET_CELL_K]; sx.extend_from_slice(&encode_float(k)); sx.push(0xF7); sx } pub fn build_sysex_get_cell_k() -> Vec { vec![0xF0, SYSEX_MFR, CMD_GET_CELL_K, 0xF7] } pub fn build_sysex_set_cl_factor(f: f32) -> Vec { 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 { vec![0xF0, SYSEX_MFR, CMD_GET_CL_FACTOR, 0xF7] } pub fn build_sysex_set_ph_cal(slope: f32, offset: f32) -> Vec { 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 { vec![0xF0, SYSEX_MFR, CMD_GET_PH_CAL, 0xF7] }