EIS-BLE-S3/cue/src/protocol.rs

607 lines
20 KiB
Rust

/// 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<Self> {
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<Self> {
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<Self> {
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<Self> {
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<u32>, meas_id: Option<u16> },
DataPoint { _index: u16, point: EisPoint },
SweepEnd,
Config(EisConfig),
LsvStart { num_points: u16, v_start: f32, v_stop: f32,
esp_timestamp: Option<u32>, meas_id: Option<u16> },
LsvPoint { _index: u16, point: LsvPoint },
LsvEnd,
AmpStart { v_hold: f32, esp_timestamp: Option<u32>, meas_id: Option<u16> },
AmpPoint { _index: u16, point: AmpPoint },
AmpEnd,
ClStart { num_points: u16, esp_timestamp: Option<u32>, meas_id: Option<u16> },
ClPoint { _index: u16, point: ClPoint },
ClResult(ClResult),
ClEnd,
PhResult(PhResult, Option<u32>, Option<u16>),
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<EisMessage> {
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<u8> {
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<u8> {
vec![0xF0, SYSEX_MFR, CMD_SET_RTIA, rtia.as_byte(), 0xF7]
}
pub fn build_sysex_set_rcal(rcal: Rcal) -> Vec<u8> {
vec![0xF0, SYSEX_MFR, CMD_SET_RCAL, rcal.as_byte(), 0xF7]
}
pub fn build_sysex_set_electrode(e: Electrode) -> Vec<u8> {
vec![0xF0, SYSEX_MFR, CMD_SET_ELECTRODE, e.as_byte(), 0xF7]
}
pub fn build_sysex_start_sweep() -> Vec<u8> {
vec![0xF0, SYSEX_MFR, CMD_START_SWEEP, 0xF7]
}
pub fn build_sysex_get_config() -> Vec<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
vec![0xF0, SYSEX_MFR, CMD_GET_TEMP, 0xF7]
}
pub fn build_sysex_start_ph(stabilize_s: f32) -> Vec<u8> {
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<u8> {
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<u8> {
vec![0xF0, SYSEX_MFR, CMD_START_REFS, 0xF7]
}
pub fn build_sysex_get_refs() -> Vec<u8> {
vec![0xF0, SYSEX_MFR, CMD_GET_REFS, 0xF7]
}
pub fn build_sysex_clear_refs() -> Vec<u8> {
vec![0xF0, SYSEX_MFR, CMD_CLEAR_REFS, 0xF7]
}
pub fn build_sysex_set_cell_k(k: f32) -> Vec<u8> {
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<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]
}
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]
}