Add BPM tracking

This commit is contained in:
Sebastian H. Gabrielli 2024-11-18 18:31:11 +01:00
parent e766b560c7
commit 898c11a67b
5 changed files with 61 additions and 14 deletions

7
src-tauri/Cargo.lock generated
View File

@ -863,6 +863,12 @@ dependencies = [
"rustc_version", "rustc_version",
] ]
[[package]]
name = "find_peaks"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17ba6fe8df99019a8cb0c712414e10081b3bbdb10d8f7ab95719863ced5db982"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.33" version = "1.0.33"
@ -3524,6 +3530,7 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"env_logger", "env_logger",
"find_peaks",
"log", "log",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",

View File

@ -23,6 +23,7 @@ log = "0.4.22"
env_logger = "0.11.5" env_logger = "0.11.5"
tokio = { version = "1.41.1", features = ["full"] } tokio = { version = "1.41.1", features = ["full"] }
thiserror = "2.0.3" thiserror = "2.0.3"
find_peaks = "0.1.5"
[features] [features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!

View File

@ -87,12 +87,12 @@ fn create_data_listener(websockets_uri: String, data: Arc<Mutex<Vec<ECGData>>>)
.into_iter() .into_iter()
.filter(|data| { .filter(|data| {
Utc::now().signed_duration_since(data.timestamp) Utc::now().signed_duration_since(data.timestamp)
<= Duration::seconds(5) <= Duration::minutes(1)
}) })
.collect(); .collect();
ecg_data.push(ECGData { ecg_data.push(ECGData {
value: voltage, value: -voltage,
timestamp: Utc::now(), timestamp: Utc::now(),
}); });
@ -107,9 +107,28 @@ fn create_data_listener(websockets_uri: String, data: Arc<Mutex<Vec<ECGData>>>)
}); });
} }
#[tauri::command]
fn calculate_heartrate(data: State<'_, Arc<Mutex<Vec<ECGData>>>>) -> Result<usize, Error> {
let data = data.lock().expect("Failed to grab mutex lock").clone();
let filtered_data: Vec<f32> = data.into_iter().map(|entry| entry.value).collect();
let mut peak_finder = find_peaks::PeakFinder::new(&filtered_data);
peak_finder.with_min_prominence(1.);
//peak_finder.with_min_height(0.);
Ok(peak_finder.find_peaks().len())
}
#[tauri::command] #[tauri::command]
async fn get_ecg_data(data: State<'_, Arc<Mutex<Vec<ECGData>>>>) -> Result<Vec<ECGData>, Error> { async fn get_ecg_data(data: State<'_, Arc<Mutex<Vec<ECGData>>>>) -> Result<Vec<ECGData>, Error> {
Ok(data.lock().expect("Failed to get lock").clone()) Ok(data
.lock()
.expect("Failed to get lock")
.clone()
.into_iter()
.filter(|data| Utc::now().signed_duration_since(data.timestamp) <= Duration::seconds(5))
.collect())
} }
#[tokio::main] #[tokio::main]
@ -120,11 +139,17 @@ async fn main() {
let ecg_data: Arc<Mutex<Vec<ECGData>>> = Arc::new(Mutex::new(Vec::with_capacity(2 ^ 13))); let ecg_data: Arc<Mutex<Vec<ECGData>>> = Arc::new(Mutex::new(Vec::with_capacity(2 ^ 13)));
// Create the websockets listener // Create the websockets listener
create_data_listener("ws://192.168.128.50:81".to_string(), ecg_data.clone()); //create_data_listener("ws://192.168.128.50:81".to_string(), ecg_data.clone());
create_data_listener("ws://192.168.66.210:81".to_string(), ecg_data.clone());
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet, random_data, get_ecg_data]) .invoke_handler(tauri::generate_handler![
greet,
random_data,
get_ecg_data,
calculate_heartrate
])
.manage(ecg_data) .manage(ecg_data)
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

@ -3,21 +3,28 @@
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import Greet from "./components/Greet.vue"; import Greet from "./components/Greet.vue";
import Graph from "./components/Graph.vue"; import Graph from "./components/Graph.vue";
/*
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { onMounted } from "vue"; import { onMounted, ref } from "vue";
let bpm = ref(0);
async function refresh_bpm() {
bpm.value = await invoke("calculate_heartrate");
}
onMounted(async () => { onMounted(async () => {
await invoke("create_data_listener", { await refresh_bpm();
websocketsUri: "ws://192.168.128.50:81",
}); setInterval(() => {
refresh_bpm();
}, 250);
}); });
*/
</script> </script>
<template> <template>
<div class="container"> <div class="container">
<Graph /> <Graph />
<h3>Pulse: {{ bpm }} bpm</h3>
</div> </div>
</template> </template>

View File

@ -32,7 +32,7 @@ export default {
datasets: [ datasets: [
{ {
// Ensure datasets is an array // Ensure datasets is an array
label: "My Dataset", // Add a label for the dataset label: "ECG Measurement", // Add a label for the dataset
data: [], // Initial data data: [], // Initial data
fill: false, // Set to true if you want the area under the line to be filled fill: false, // Set to true if you want the area under the line to be filled
borderColor: "blue", // Set the line color borderColor: "blue", // Set the line color
@ -51,9 +51,16 @@ export default {
x: { x: {
ticks: { ticks: {
callback: (value, index, values) => { callback: (value, index, values) => {
/*
// Show only 5 timestamps on the x-axis // Show only 5 timestamps on the x-axis
const step = Math.floor(values.length / 5); const step = Math.floor(values.length / 5);
return index % step === 0 ? chartData.value.labels[index] : ""; // Show the label only for selected indices return index % step === 0 ? chartData.value.labels[index] : ""; // Show the label only for selected indices
*/
const totalLabels = values.length;
const step = Math.ceil(totalLabels / 5); // Use ceil to ensure we cover all labels
return index % step === 0 && index < totalLabels
? chartData.value.labels[index]
: ""; // Show the label only for selected indices
}, },
}, },
}, },
@ -75,7 +82,7 @@ export default {
labels: timestamps, labels: timestamps,
datasets: [ datasets: [
{ {
label: "My Dataset", // Add a label for the dataset label: "ECG Measurement", // Add a label for the dataset
data: values, // Access the data property data: values, // Access the data property
fill: false, // Set to true if you want the area under the line to be filled fill: false, // Set to true if you want the area under the line to be filled
borderColor: "green", // Set the line color borderColor: "green", // Set the line color
@ -103,7 +110,7 @@ export default {
labels: timestamps, labels: timestamps,
datasets: [ datasets: [
{ {
label: "My Dataset", label: "ECG Measurement",
data: values, data: values,
fill: false, fill: false,
borderColor: "green", borderColor: "green",