From cc60f6990eedde52a722297f834bf411f189e64e Mon Sep 17 00:00:00 2001 From: KubaWis Date: Thu, 12 Feb 2026 19:16:36 +0100 Subject: [PATCH] add plot renderer and config options --- Cargo.toml | 2 + config.toml | 4 + config.toml_meine | 4 + plotters_test/Cargo.toml | 8 ++ plotters_test/src/main.rs | 119 ++++++++++++++++++++++++++++ src/main.rs | 161 +++++++++++++++++++++++++++++--------- src/miband8.rs | 16 +++- 7 files changed, 274 insertions(+), 40 deletions(-) create mode 100644 config.toml create mode 100644 config.toml_meine create mode 100644 plotters_test/Cargo.toml create mode 100644 plotters_test/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index a81b5c2..2bd3060 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,9 @@ edition = "2021" aes = "0.8.4" btleplug = "0.11.6" ccm = "0.5.0" +config = "0.15.18" futures = "0.3.31" +glob = "0.3.3" hex = "0.4.3" hmac = "0.12.1" protobuf = "3.7.1" diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..d1fd7eb --- /dev/null +++ b/config.toml @@ -0,0 +1,4 @@ +watch_key = "f60b07bd3739018d7ee61d866c1eb01d" +send_to_chat = false +send_addr = "172.16.0.21:9000" +parameters = ["/avatar/parameters/VF101_beat","/avatar/parameters/VF142_beat","/avatar/parameters/VF145_beat","/avatar/parameters/beat"] diff --git a/config.toml_meine b/config.toml_meine new file mode 100644 index 0000000..d1fd7eb --- /dev/null +++ b/config.toml_meine @@ -0,0 +1,4 @@ +watch_key = "f60b07bd3739018d7ee61d866c1eb01d" +send_to_chat = false +send_addr = "172.16.0.21:9000" +parameters = ["/avatar/parameters/VF101_beat","/avatar/parameters/VF142_beat","/avatar/parameters/VF145_beat","/avatar/parameters/beat"] diff --git a/plotters_test/Cargo.toml b/plotters_test/Cargo.toml new file mode 100644 index 0000000..2022c90 --- /dev/null +++ b/plotters_test/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "plotters_test" +version = "0.1.0" +edition = "2024" + +[dependencies] +chrono = "0.4.40" +plotters = "0.3.7" diff --git a/plotters_test/src/main.rs b/plotters_test/src/main.rs new file mode 100644 index 0000000..8a7e67d --- /dev/null +++ b/plotters_test/src/main.rs @@ -0,0 +1,119 @@ +use chrono::{TimeZone, Utc}; +use plotters::prelude::*; +use std::fs::File; +use std::i32; +use std::io::{self, BufRead}; +use std::path::Path; +use std::error::Error; + +fn read_lines

(filename: P) -> io::Result>> +where P: AsRef, { + let file = File::open(filename)?; + Ok(io::BufReader::new(file).lines()) +} + +const ZONES: [(u8,i32,i32,&str, u32); 6] = [ + (0,0,100, "Chill", 0x24d0e5), + (1,100,120, "Light", 0x2498e5), + (2,120,140, "Intensive", 0x19b854), + (3,140,160, "Aerobix", 0xefab00), + (4,160,180, "Anaerobic", 0xf58201), + (5,180,255, "VO2 Max", 0xf03e3e), +]; + +fn hr_to_zone(hr: i32) -> u8 { + for z in ZONES { + if hr < z.2 { + return z.0; + } + } + ZONES[0].0 +} + +const OUT_FILE_NAME: &str = "plotters-doc-data/slc-temp.png"; +fn main() -> Result<(), Box> { + + let mut raw_data: Vec<(i32, i32)> = vec![]; + + let mut min = i32::MAX; + let mut max = 0; + + if let Ok(lines) = read_lines("hr.csv") { + // Consumes the iterator, returns an (Optional) String + for line in lines.map_while(Result::ok) { + let mut split = line.split(", "); + let timestp = split.nth(0).unwrap().parse::().unwrap(); + let hr = split.nth(0).unwrap().parse::().unwrap(); + raw_data.push((timestp, hr)); + if hr < min { + min = hr; + } + if hr > max { + max = hr; + } + } + } + println!("min: {}, max: {}", min, max); + let mut zones: Vec> = vec![]; + let mut prev = 255; + + for data in &raw_data { + let zone = hr_to_zone(data.1); + if zone != prev { + if prev != 255 { + zones.last_mut().unwrap().push((zone, data.0 , data.1)); + } + zones.push(vec![(zone, data.0 , data.1)]) + } + zones.last_mut().unwrap().push((zone, data.0 , data.1)); + + prev = zone; + } + + + let root = BitMapBackend::new(OUT_FILE_NAME, (raw_data.len()as u32*2 , 1024*3)).into_drawing_area(); + + root.fill(&BLACK)?; + + let mut chart = ChartBuilder::on(&root) + .margin(30) + .caption( + "Heartrate", + ("Ubuntu", 140, &WHITE), + ) + .set_label_area_size(LabelAreaPosition::Left, 60) + .set_label_area_size(LabelAreaPosition::Right, 60) + .set_label_area_size(LabelAreaPosition::Bottom, 40) + .build_cartesian_2d( + zones[0][0].1..zones.last().unwrap().last().unwrap().1, + ((min/10)*10-10)..((max/10)*10 + 10), + )?; + chart + .configure_mesh() + .disable_x_mesh() + //.disable_y_mesh() + .x_labels((((max/10)+ 1) - ((min/10)-1))as usize*2 ) + .max_light_lines(2) + .y_desc("BPM") + .axis_style(&WHITE) + .light_line_style(&WHITE) + .bold_line_style(&WHITE) + .label_style(("Ubuntu", 50, &WHITE)) + .draw()?; + + for zone in zones { + let r: u8 = (ZONES[zone[0].0 as usize].4 >> 16 & 0xFF) as u8; + let g: u8 = (ZONES[zone[0].0 as usize].4 >> 8 & 0xFF) as u8; + let b: u8 = (ZONES[zone[0].0 as usize].4 & 0xFF) as u8; + + chart.draw_series(LineSeries::new( + zone.iter().map(|x| (x.1 as i32, x.2)), + ShapeStyle{ color: RGBColor(r,g,b).into(), filled: true, stroke_width: 4 } , + ))?; + } + + // To avoid the IO failure being ignored silently, we manually call the present function + root.present().expect("Unable to write result to file, please make sure 'plotters-doc-data' dir exists under current dir"); + println!("Result has been saved to {}", OUT_FILE_NAME); + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index fbc2e9a..c965145 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,54 +1,141 @@ mod constants; mod miband8; -mod xiaomi; mod protobuf_decoder; -use std::{net::{SocketAddrV4, UdpSocket}, str::FromStr}; +use config::Config; use rosc::{encoder, OscMessage, OscPacket, OscType}; -use tokio::{sync::watch, task}; -const SEND_TO_CHAT: bool = false; -const SEND_ADDR: &str = "10.42.0.2:9000"; +use std::{ + collections::HashMap, fs::OpenOptions, io::Write, net::{SocketAddrV4, UdpSocket}, str::FromStr, sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH} +}; + +use tokio::{ + sync::{watch, Mutex}, + task, + time::sleep, +}; + +use crate::miband8::Miband8; + +const SEND_TO_CHAT: bool = false; +const SEND_ADDR: &str = "172.16.0.21:9000"; #[tokio::main] async fn main() { - let send_addr: SocketAddrV4 = SocketAddrV4::from_str(SEND_ADDR).unwrap(); - - - let sock = UdpSocket::bind(SocketAddrV4::from_str("0.0.0.0:9000").unwrap()).unwrap(); + + let settings = Config::builder() + .add_source(config::File::with_name("config.toml")) + .build() + .unwrap(); + + + let send_addr: SocketAddrV4 = SocketAddrV4::from_str(&settings.get_string("send_addr").unwrap()).unwrap(); + + let sock = UdpSocket::bind(SocketAddrV4::from_str("0.0.0.0:0").unwrap()).unwrap(); let (tx, mut rx) = watch::channel::(0); - - let mut band = miband8::Miband8::new("f60b07bd3739018d7ee61d866c1eb01d".to_owned()); - let e = band.init().await; - println!("{:?}", e); + let send_to_chat = settings.get_bool("send_to_chat").unwrap(); + let param_names = settings.get_array("parameters").unwrap().iter().map(|a| a.clone().into_string().unwrap()).collect::>(); task::spawn(async move { - band.continuously_measure_hr(tx).await; - }); - while rx.changed().await.is_ok() { - let hr = rx.borrow_and_update().clone(); - println!("Got heartrate: {}", hr); - - if SEND_TO_CHAT { - let msg_buf = encoder::encode(&OscPacket::Message(OscMessage { - addr: "/chatbox/input".to_string(), - args: vec![OscType::String(format!("<3 {hr} bpm").to_string()), rosc::OscType::Bool(true),rosc::OscType::Bool(false)], - })) + let mut file = OpenOptions::new() + .write(true) + .truncate(true) + .open("./hr.csv") .unwrap(); - - sock.send_to(&msg_buf, send_addr).unwrap(); - } - let param_names = vec!["/avatar/parameters/VF101_beat", "/avatar/parameters/VF142_beat", "/avatar/parameters/VF145_beat"]; - for param in param_names { - let msg_buf = encoder::encode(&OscPacket::Message(OscMessage { - addr: param.to_string(), - args: vec![OscType::Float((hr as f32 - 127.0)/ 127.0)], - })) - .unwrap(); - sock.send_to(&msg_buf, send_addr).unwrap(); + while rx.changed().await.is_ok() { + let hr = rx.borrow_and_update().clone(); + println!("Got heartrate: {}", hr); + if hr == 0 { + continue; + }; + let start = SystemTime::now(); + let since_the_epoch = start + .duration_since(UNIX_EPOCH) + .expect("Time went backwards"); + + if let Err(e) = writeln!(file, "{}", format!("{}, {hr}", since_the_epoch.as_secs())) { + println!("Couldnt write to file: {}", e) + } + if send_to_chat { + let msg_buf = encoder::encode(&OscPacket::Message(OscMessage { + addr: "/chatbox/input".to_string(), + args: vec![ + OscType::String(format!("<3 {hr} bpm").to_string()), + rosc::OscType::Bool(true), + rosc::OscType::Bool(false), + ], + })) + .unwrap(); + + sock.send_to(&msg_buf, send_addr).unwrap(); + } + for param in ¶m_names { + let msg_buf = encoder::encode(&OscPacket::Message(OscMessage { + addr: param.to_string(), + args: vec![OscType::Float((hr as f32 - 127.0) / 127.0)], + })) + .unwrap(); + + sock.send_to(&msg_buf, send_addr).unwrap(); + } } + }); + + let connected: Arc> = Arc::new(Mutex::new(false)); + + let band: Arc>> = Arc::new(Mutex::new(None)); + + let band_c = band.clone(); + let connected_a = connected.clone(); + task::spawn(async move { + let band_cc = band_c.clone(); + let tx_c = tx.clone(); + let mut task = task::spawn(async move { + let mut a = band_cc.lock().await.clone(); + a.as_mut().unwrap().continuously_measure_hr(tx_c).await; + }); + loop { + let mut band_x = band_c.lock().await.clone(); + let mut conec = connected_a.lock().await; + if *conec { + if let Err(e) = band_x.as_mut().unwrap().start_measure().await { + println!("Disconnected: {e}"); + *conec = false; + task.abort(); + } + if task.is_finished() { + let band_cc = band_c.clone(); + let tx = tx.clone(); + task = task::spawn(async move { + let mut a = band_cc.lock().await.clone(); + a.as_mut().unwrap().continuously_measure_hr(tx).await; + }); + } + } + drop(conec); + sleep(Duration::from_secs(10)).await; + // println!("118 loop"); + } + }); + + + let band_c = band.clone(); + let connected_a: Arc> = connected.clone(); + let key = settings.get_string("watch_key").unwrap(); + loop { + let mut band_x: tokio::sync::MutexGuard<'_, Option> = band_c.lock().await; + if !*connected_a.lock().await { + println!("Trying to connect"); + let mut band = miband8::Miband8::new(key.to_owned()); + if let Ok(_) = band.init().await { + *band_x = Some(band); + *connected_a.lock().await = true; + }else { + sleep(Duration::from_secs(10)).await; + } + } + sleep(Duration::from_secs(1)).await; + // println!("137 loop"); } - - } diff --git a/src/miband8.rs b/src/miband8.rs index 3b4df34..62b18c3 100644 --- a/src/miband8.rs +++ b/src/miband8.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::time::SystemTime; use std::{error::Error, time::Duration, vec}; use btleplug::{api::{Characteristic, Service,Central, Manager as _, Peripheral as _, ScanFilter, WriteType}, platform::{self, Adapter, Peripheral}}; @@ -21,6 +22,8 @@ use crate::protobuf_decoder::{self, Value}; type HmacSha256 = Hmac; type Aes128Ccm = Ccm; + +#[derive(Clone)] pub struct Miband8 { dev: Option, central: Option, @@ -195,13 +198,19 @@ impl Miband8 { self.handle } - pub async fn continuously_measure_hr(&mut self, channel_tx: Sender) -> u8 { + pub async fn start_measure(&mut self) -> Result<() ,Box> { let command: Vec = vec![0x08, 0x08, 0x10, 0x2d]; let handle = self.get_handle(); let mut command_enc = vec![0x00, 0x00, 0x02, 0x01, handle, 0x00]; command_enc.append(&mut self.encrypt(self.encryption_key.clone(), &mut self.encryption_nonce.clone(), command, handle)); - self.dev.as_ref().unwrap().write(&self.chars.get("write").unwrap(), &command_enc, WriteType::WithoutResponse).await.unwrap(); - + if let Err(e) = self.dev.as_ref().unwrap().write(&self.chars.get("write").unwrap(), &command_enc, WriteType::WithoutResponse).await { + return Err(e.to_string().into()); + } + Ok(()) + } + + pub async fn continuously_measure_hr(&mut self, channel_tx: Sender) -> u8 { + let start_time = SystemTime::now(); let mut events2 = self.dev.as_ref().unwrap().notifications().await.unwrap(); while let Some(event) = events2.next().await { self.ack().await.unwrap(); @@ -218,6 +227,7 @@ impl Miband8 { 0 } + async fn handle_watch_nonce(&mut self, notification_value: &Vec) -> Result, Box> { let watch_nonce: Vec = notification_value[15..31].to_vec(); let watch_hmac: Vec = notification_value[33..65].to_vec();