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();