Pass electrode config from GUI to hardware.

This commit is contained in:
2026-03-02 19:06:00 +01:00
parent 04d80d4968
commit 27ee40e724
6 changed files with 228 additions and 48 deletions

View File

@@ -1,11 +1,13 @@
use std::fmt::Debug;
use log::{info, error};
use core::f32;
use std::f32::consts::PI;
// use std::f32::consts::PI;
use std::ops::RangeInclusive;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::Ordering;
use chrono::Local;
@@ -22,13 +24,56 @@ use crate::plot::{TimeSeriesPlot, BodePlot};
use crate::signals::{LoggingSignal, StartStopSignal};
use crate::icd::{BioImpedanceLeadMode, IcdDftNum, MeasurementPointSet};
use crate::icd::{BioImpedanceLeadMode, IcdDftNum, MeasurementPointSet, ElectrodeConfiguration, ElectrodeOptionsWithMultiplexer};
const LEAD_MODES: [BioImpedanceLeadMode; 2] = [
BioImpedanceLeadMode::TwoLead,
BioImpedanceLeadMode::FourLead,
];
struct ElectrodeSettings {
with_multiplexer_2_lead: [ElectrodeOptionsWithMultiplexer; 2],
with_multiplexer_4_lead: [ElectrodeOptionsWithMultiplexer; 4],
}
impl ElectrodeSettings {
pub fn new() -> Self {
Self {
with_multiplexer_2_lead: [
ElectrodeOptionsWithMultiplexer::E1, // Electrode+ default
ElectrodeOptionsWithMultiplexer::E23, // Electrode- default
],
with_multiplexer_4_lead: [
ElectrodeOptionsWithMultiplexer::E1, // I+ default
ElectrodeOptionsWithMultiplexer::E23, // I- default
ElectrodeOptionsWithMultiplexer::E10, // V+ default
ElectrodeOptionsWithMultiplexer::E12, // V- default
],
}
}
pub fn to_electrode_config(
&self,
hardware_connected: HardwareConnected,
lead_mode: BioImpedanceLeadMode,
) -> Option<ElectrodeConfiguration> {
if hardware_connected != HardwareConnected::WithMultiplexer {
return None;
}
match lead_mode {
BioImpedanceLeadMode::TwoLead => {
let [a, b] = self.with_multiplexer_2_lead;
Some(ElectrodeConfiguration::WithMultiplexer2Lead(a, b))
}
BioImpedanceLeadMode::FourLead => {
let [a, b, c, d] = self.with_multiplexer_4_lead;
Some(ElectrodeConfiguration::WithMultiplexer4Lead(a, b, c, d))
}
}
}
}
const DFTNUM_VARIANTS: [IcdDftNum; 13] = [
IcdDftNum::Num4, IcdDftNum::Num8, IcdDftNum::Num16, IcdDftNum::Num32,
IcdDftNum::Num64, IcdDftNum::Num128, IcdDftNum::Num256, IcdDftNum::Num512,
@@ -48,6 +93,13 @@ enum TabActive {
Shortcuts,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HardwareConnected {
None,
WithoutMultiplexer,
WithMultiplexer,
}
pub struct App {
tree: DockState<String>,
tab_viewer: TabViewer,
@@ -58,12 +110,13 @@ pub struct App {
pub magnitude_series: Arc<Mutex<TimeSeriesPlot>>,
pub phase_series: Arc<Mutex<TimeSeriesPlot>>,
pub bode_plot: Arc<Mutex<BodePlot>>,
pub connected: Arc<AtomicBool>,
pub hardware_connected: Arc<Mutex<HardwareConnected>>,
pub on: Arc<Mutex<bool>>,
tab_active: TabActive,
pub data_frequency: Arc<AtomicF32>,
pub single_frequency: Arc<Mutex<u32>>,
pub lead_mode: Arc<Mutex<BioImpedanceLeadMode>>,
electrode_settings: Arc<Mutex<ElectrodeSettings>>,
pub dft_num: Arc<Mutex<IcdDftNum>>,
pub measurement_points: Arc<Mutex<MeasurementPointSet>>,
pub periods_per_dft: Arc<Mutex<Option<f32>>>,
@@ -82,7 +135,9 @@ struct TabViewer {
bode_plot: Arc<Mutex<BodePlot>>,
on: Arc<Mutex<bool>>,
single_frequency: Arc<Mutex<u32>>,
hardware_connected: Arc<Mutex<HardwareConnected>>,
lead_mode: Arc<Mutex<BioImpedanceLeadMode>>,
electrode_settings: Arc<Mutex<ElectrodeSettings>>,
dft_num: Arc<Mutex<IcdDftNum>>,
measurement_points: Arc<Mutex<MeasurementPointSet>>,
periods_per_dft: Arc<Mutex<Option<f32>>>,
@@ -91,6 +146,60 @@ struct TabViewer {
show_settings_toggle: Option<bool>,
}
trait ElectrodeOption: Copy + Debug + PartialEq + 'static {
const ALL: &'static [Self];
}
impl ElectrodeOption for ElectrodeOptionsWithMultiplexer {
const ALL: &'static [Self] = &ElectrodeOptionsWithMultiplexer::ALL;
}
fn electrode_combo<T: ElectrodeOption>(
ui: &mut egui::Ui,
id: &str,
value: &mut T,
) {
egui::ComboBox::from_id_salt(id)
.selected_text(format!("{:?}", value))
.width(60.0)
.show_ui(ui, |ui| {
for &option in T::ALL {
ui.selectable_value(value, option, format!("{:?}", option));
}
});
}
fn render_two_lead<T: ElectrodeOption>(
ui: &mut egui::Ui,
values: &mut [T; 2],
) {
ui.horizontal(|ui| {
ui.label("Drive/sense: Electrode+ (");
electrode_combo(ui, "e1_select", &mut values[0]);
ui.label("), Electrode- (");
electrode_combo(ui, "e2_select", &mut values[1]);
ui.label(")");
});
}
fn render_four_lead<T: ElectrodeOption>(
ui: &mut egui::Ui,
values: &mut [T; 4],
) {
ui.horizontal(|ui| {
ui.label("Drive: I+ (");
electrode_combo(ui, "i1_select", &mut values[0]);
ui.label("), I- (");
electrode_combo(ui, "i2_select", &mut values[1]);
ui.label(") | Sense: V+ (");
electrode_combo(ui, "v1_select", &mut values[2]);
ui.label("), V- (");
electrode_combo(ui, "v2_select", &mut values[3]);
ui.label(")");
});
}
impl TabViewer {
fn single_tab(&mut self, ui: &mut egui::Ui) {
egui::Frame::default().inner_margin(5).show(ui, |ui| {
@@ -125,11 +234,28 @@ impl TabViewer {
*lead_mode = LEAD_MODES[index];
info!("Lead Mode setting changed!");
}
});
ui.horizontal(|ui| {
// Show lead configuration
match *lead_mode {
BioImpedanceLeadMode::TwoLead => ui.label("Drive/sense: Electrode+ (CE0), Electrode- (AIN1)"),
BioImpedanceLeadMode::FourLead => ui.label("Drive: I+ (CE0), I- (AIN1) | Sense: V+ (AIN2), V- (AIN3)"),
ui.label("Lead Configuration:");
let hardware_connected = self.hardware_connected.lock().unwrap();
let lead_mode = self.lead_mode.lock().unwrap();
let mut settings = self.electrode_settings.lock().unwrap();
match (*hardware_connected, *lead_mode) {
(HardwareConnected::WithoutMultiplexer, BioImpedanceLeadMode::TwoLead) => {
ui.label(format!("Drive/sense: Electrode+ (CE0), Electrode- (AIN1)"));
}
(HardwareConnected::WithoutMultiplexer, BioImpedanceLeadMode::FourLead) => {
ui.label(format!("Drive: I+ (CE0), I- (AIN1) | Sense: V+ (AIN2), V- (AIN3)"));
}
(HardwareConnected::WithMultiplexer, BioImpedanceLeadMode::TwoLead) => {
render_two_lead(ui, &mut settings.with_multiplexer_2_lead);
}
(HardwareConnected::WithMultiplexer, BioImpedanceLeadMode::FourLead) => {
render_four_lead(ui, &mut settings.with_multiplexer_4_lead);
}
(HardwareConnected::None, _) => {}
}
});
});
@@ -259,11 +385,28 @@ impl TabViewer {
*lead_mode = LEAD_MODES[index];
info!("Lead Mode setting changed!");
}
});
ui.horizontal(|ui| {
// Show lead configuration
match *lead_mode {
BioImpedanceLeadMode::TwoLead => ui.label("Drive/sense: Electrode+ (CE0), Electrode- (AIN1)"),
BioImpedanceLeadMode::FourLead => ui.label("Drive: I+ (CE0), I- (AIN1) | Sense: V+ (AIN2), V- (AIN3)"),
ui.label("Lead Configuration:");
let hardware_connected = self.hardware_connected.lock().unwrap();
let lead_mode = self.lead_mode.lock().unwrap();
let mut settings = self.electrode_settings.lock().unwrap();
match (*hardware_connected, *lead_mode) {
(HardwareConnected::WithoutMultiplexer, BioImpedanceLeadMode::TwoLead) => {
ui.label(format!("Drive/sense: Electrode+ (CE0), Electrode- (AIN1)"));
}
(HardwareConnected::WithoutMultiplexer, BioImpedanceLeadMode::FourLead) => {
ui.label("Drive: I+ (CE0), I- (AIN1) | Sense: V+ (AIN2), V- (AIN3)");
}
(HardwareConnected::WithMultiplexer, BioImpedanceLeadMode::TwoLead) => {
render_two_lead(ui, &mut settings.with_multiplexer_2_lead);
}
(HardwareConnected::WithMultiplexer, BioImpedanceLeadMode::FourLead) => {
render_four_lead(ui, &mut settings.with_multiplexer_4_lead);
}
(HardwareConnected::None, _) => {}
}
});
});
@@ -509,7 +652,9 @@ impl App {
let phase_series = Arc::new(Mutex::new(TimeSeriesPlot::new()));
let bode_plot = Arc::new(Mutex::new(BodePlot::new()));
let single_frequency = Arc::new(Mutex::new(50000));
let hardware_connected = Arc::new(Mutex::new(HardwareConnected::None));
let lead_mode = Arc::new(Mutex::new(BioImpedanceLeadMode::FourLead));
let electrode_settings = Arc::new(Mutex::new(ElectrodeSettings::new()));
let dft_num = Arc::new(Mutex::new(IcdDftNum::Num2048));
let measurement_points = Arc::new(Mutex::new(MeasurementPointSet::Eighteen));
let periods_per_dft = Arc::new(Mutex::new(None));
@@ -525,7 +670,9 @@ impl App {
phase_series: phase_series.clone(),
bode_plot: bode_plot.clone(),
single_frequency: single_frequency.clone(),
hardware_connected: hardware_connected.clone(),
lead_mode: lead_mode.clone(),
electrode_settings: electrode_settings.clone(),
dft_num: dft_num.clone(),
measurement_points: measurement_points.clone(),
periods_per_dft: periods_per_dft.clone(),
@@ -546,12 +693,13 @@ impl App {
magnitude_series,
phase_series,
bode_plot,
connected: Arc::new(AtomicBool::new(false)),
hardware_connected,
on,
tab_active,
data_frequency: Arc::new(AtomicF32::new(0.0)),
single_frequency,
lead_mode,
electrode_settings,
dft_num,
measurement_points,
periods_per_dft,
@@ -587,12 +735,17 @@ impl App {
pub fn update_start_stop(&self) {
match (self.tab_active, *self.on.lock().unwrap()) {
(TabActive::Single, true) => {
if let Err(e) = self.run_impedancemeter_tx.try_send(StartStopSignal::StartSingle(*self.single_frequency.lock().unwrap(), *self.lead_mode.lock().unwrap(), *self.dft_num.lock().unwrap())) {
let lead_mode = *self.lead_mode.lock().unwrap();
let electrode_config = self.electrode_settings.lock().unwrap().to_electrode_config(*self.hardware_connected.lock().unwrap(), lead_mode);
if let Err(e) = self.run_impedancemeter_tx.try_send(
StartStopSignal::StartSingle(*self.single_frequency.lock().unwrap(), lead_mode, electrode_config, *self.dft_num.lock().unwrap())) {
error!("Failed to send start command: {:?}", e);
}
},
(TabActive::Sweep, true) => {
if let Err(e) = self.run_impedancemeter_tx.try_send(StartStopSignal::StartSweep(*self.lead_mode.lock().unwrap(), *self.measurement_points.lock().unwrap())) {
let lead_mode = *self.lead_mode.lock().unwrap();
let electrode_config = self.electrode_settings.lock().unwrap().to_electrode_config(*self.hardware_connected.lock().unwrap(), lead_mode);
if let Err(e) = self.run_impedancemeter_tx.try_send(StartStopSignal::StartSweep(lead_mode, electrode_config, *self.measurement_points.lock().unwrap())) {
error!("Failed to send start command: {:?}", e);
}
},
@@ -616,7 +769,7 @@ impl eframe::App for App {
// Egui add a top bar
egui::TopBottomPanel::top("top_bar").show(ctx, |ui| {
egui::MenuBar::new().ui(ui, |ui| {
let connected = self.connected.load(Ordering::Relaxed);
let is_connected = *self.hardware_connected.lock().unwrap() != HardwareConnected::None;
egui::widgets::global_theme_preference_switch(ui);
ui.separator();
@@ -641,26 +794,26 @@ impl eframe::App for App {
.min_size(egui::vec2(45.0, 0.0))
.frame(true);
if ui.add_enabled(connected, start_stop_button).clicked() {
if ui.add_enabled(is_connected, start_stop_button).clicked() {
*self.on.lock().unwrap() = !on;
self.update_start_stop();
}
ui.separator();
if ui.add_enabled(connected, Button::new("Reset view")).clicked() {
if ui.add_enabled(is_connected, Button::new("Reset view")).clicked() {
self.reset_view();
}
ui.separator();
ui.add_enabled(connected, Label::new("Logging:"));
ui.add_enabled(is_connected, Label::new("Logging:"));
let gui_logging_state = *self.gui_logging_state.lock().unwrap();
let mut logging_enabled = !matches!(gui_logging_state, LoggingStates::Idle);
if ui.add_enabled(connected, toggle_start_stop(&mut logging_enabled)).changed() {
if ui.add_enabled(is_connected, toggle_start_stop(&mut logging_enabled)).changed() {
let signal = match gui_logging_state {
LoggingStates::Idle => LoggingSignal::StartFileLogging(self.log_filename.clone()),
LoggingStates::Starting | LoggingStates::Logging => LoggingSignal::StopFileLogging,
@@ -673,7 +826,7 @@ impl eframe::App for App {
ui.separator();
ui.add_enabled_ui(on && logging_enabled, |ui| {
ui.add_enabled_ui(is_connected && logging_enabled, |ui| {
if Button::new("Add marker").corner_radius(5.0).min_size(egui::vec2(20.0, 0.0)).ui(ui).on_hover_text("Add a marker to the current time series plots").clicked() {
self.log_marker_modal = true;
}
@@ -736,9 +889,17 @@ impl eframe::App for App {
// Spacer to push the LED to the right
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.scope(|ui| {
let is_connected = self.connected.load(Ordering::Relaxed);
let color = if is_connected { Color32::DARK_GREEN } else { Color32::DARK_RED };
let tooltip = if is_connected { "Connected" } else { "Disconnected" };
let (color, tooltip) = match *self.hardware_connected.lock().unwrap() {
HardwareConnected::None => {
(Color32::DARK_RED, "Disconnected")
},
HardwareConnected::WithoutMultiplexer => {
(Color32::DARK_GREEN, "Connected without multiplexer")
},
HardwareConnected::WithMultiplexer => {
(Color32::DARK_GREEN, "Connected with multiplexer")
},
};
// Allocate a fixed-size rectangle for the LED
let led_size = egui::Vec2::splat(12.0);