add plot renderer and config options
This commit is contained in:
parent
20987a66b4
commit
cc60f6990e
@ -7,7 +7,9 @@ edition = "2021"
|
|||||||
aes = "0.8.4"
|
aes = "0.8.4"
|
||||||
btleplug = "0.11.6"
|
btleplug = "0.11.6"
|
||||||
ccm = "0.5.0"
|
ccm = "0.5.0"
|
||||||
|
config = "0.15.18"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
|
glob = "0.3.3"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
protobuf = "3.7.1"
|
protobuf = "3.7.1"
|
||||||
|
|||||||
4
config.toml
Normal file
4
config.toml
Normal file
@ -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"]
|
||||||
4
config.toml_meine
Normal file
4
config.toml_meine
Normal file
@ -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"]
|
||||||
8
plotters_test/Cargo.toml
Normal file
8
plotters_test/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "plotters_test"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = "0.4.40"
|
||||||
|
plotters = "0.3.7"
|
||||||
119
plotters_test/src/main.rs
Normal file
119
plotters_test/src/main.rs
Normal file
@ -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<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
|
||||||
|
where P: AsRef<Path>, {
|
||||||
|
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<dyn Error>> {
|
||||||
|
|
||||||
|
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::<i32>().unwrap();
|
||||||
|
let hr = split.nth(0).unwrap().parse::<i32>().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<(u8,i32,i32)>> = 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(())
|
||||||
|
}
|
||||||
121
src/main.rs
121
src/main.rs
@ -1,54 +1,141 @@
|
|||||||
mod constants;
|
mod constants;
|
||||||
mod miband8;
|
mod miband8;
|
||||||
mod xiaomi;
|
|
||||||
mod protobuf_decoder;
|
mod protobuf_decoder;
|
||||||
|
|
||||||
use std::{net::{SocketAddrV4, UdpSocket}, str::FromStr};
|
use config::Config;
|
||||||
use rosc::{encoder, OscMessage, OscPacket, OscType};
|
use rosc::{encoder, OscMessage, OscPacket, OscType};
|
||||||
use tokio::{sync::watch, task};
|
|
||||||
|
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_TO_CHAT: bool = false;
|
||||||
const SEND_ADDR: &str = "10.42.0.2:9000";
|
const SEND_ADDR: &str = "172.16.0.21:9000";
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let send_addr: SocketAddrV4 = SocketAddrV4::from_str(SEND_ADDR).unwrap();
|
|
||||||
|
let settings = Config::builder()
|
||||||
|
.add_source(config::File::with_name("config.toml"))
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
|
||||||
let sock = UdpSocket::bind(SocketAddrV4::from_str("0.0.0.0:9000").unwrap()).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::<u8>(0);
|
let (tx, mut rx) = watch::channel::<u8>(0);
|
||||||
|
let send_to_chat = settings.get_bool("send_to_chat").unwrap();
|
||||||
let mut band = miband8::Miband8::new("f60b07bd3739018d7ee61d866c1eb01d".to_owned());
|
let param_names = settings.get_array("parameters").unwrap().iter().map(|a| a.clone().into_string().unwrap()).collect::<Vec<String>>();
|
||||||
let e = band.init().await;
|
|
||||||
println!("{:?}", e);
|
|
||||||
task::spawn(async move {
|
task::spawn(async move {
|
||||||
band.continuously_measure_hr(tx).await;
|
let mut file = OpenOptions::new()
|
||||||
});
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open("./hr.csv")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
while rx.changed().await.is_ok() {
|
while rx.changed().await.is_ok() {
|
||||||
let hr = rx.borrow_and_update().clone();
|
let hr = rx.borrow_and_update().clone();
|
||||||
println!("Got heartrate: {}", hr);
|
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 SEND_TO_CHAT {
|
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 {
|
let msg_buf = encoder::encode(&OscPacket::Message(OscMessage {
|
||||||
addr: "/chatbox/input".to_string(),
|
addr: "/chatbox/input".to_string(),
|
||||||
args: vec![OscType::String(format!("<3 {hr} bpm").to_string()), rosc::OscType::Bool(true),rosc::OscType::Bool(false)],
|
args: vec![
|
||||||
|
OscType::String(format!("<3 {hr} bpm").to_string()),
|
||||||
|
rosc::OscType::Bool(true),
|
||||||
|
rosc::OscType::Bool(false),
|
||||||
|
],
|
||||||
}))
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
sock.send_to(&msg_buf, send_addr).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 ¶m_names {
|
||||||
for param in param_names {
|
|
||||||
let msg_buf = encoder::encode(&OscPacket::Message(OscMessage {
|
let msg_buf = encoder::encode(&OscPacket::Message(OscMessage {
|
||||||
addr: param.to_string(),
|
addr: param.to_string(),
|
||||||
args: vec![OscType::Float((hr as f32 - 127.0)/ 127.0)],
|
args: vec![OscType::Float((hr as f32 - 127.0) / 127.0)],
|
||||||
}))
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
sock.send_to(&msg_buf, send_addr).unwrap();
|
sock.send_to(&msg_buf, send_addr).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let connected: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
|
||||||
|
|
||||||
|
let band: Arc<Mutex<Option<Miband8>>> = 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<Mutex<bool>> = connected.clone();
|
||||||
|
let key = settings.get_string("watch_key").unwrap();
|
||||||
|
loop {
|
||||||
|
let mut band_x: tokio::sync::MutexGuard<'_, Option<Miband8>> = 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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::time::SystemTime;
|
||||||
use std::{error::Error, time::Duration, vec};
|
use std::{error::Error, time::Duration, vec};
|
||||||
|
|
||||||
use btleplug::{api::{Characteristic, Service,Central, Manager as _, Peripheral as _, ScanFilter, WriteType}, platform::{self, Adapter, Peripheral}};
|
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<Sha256>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
type Aes128Ccm = Ccm<Aes128, U4, U12>;
|
type Aes128Ccm = Ccm<Aes128, U4, U12>;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Miband8 {
|
pub struct Miband8 {
|
||||||
dev: Option<Peripheral>,
|
dev: Option<Peripheral>,
|
||||||
central: Option<Adapter>,
|
central: Option<Adapter>,
|
||||||
@ -195,13 +198,19 @@ impl Miband8 {
|
|||||||
self.handle
|
self.handle
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn continuously_measure_hr(&mut self, channel_tx: Sender<u8>) -> u8 {
|
pub async fn start_measure(&mut self) -> Result<() ,Box<dyn Error + Send + Sync>> {
|
||||||
let command: Vec<u8> = vec![0x08, 0x08, 0x10, 0x2d];
|
let command: Vec<u8> = vec![0x08, 0x08, 0x10, 0x2d];
|
||||||
let handle = self.get_handle();
|
let handle = self.get_handle();
|
||||||
let mut command_enc = vec![0x00, 0x00, 0x02, 0x01, handle, 0x00];
|
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));
|
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>) -> u8 {
|
||||||
|
let start_time = SystemTime::now();
|
||||||
let mut events2 = self.dev.as_ref().unwrap().notifications().await.unwrap();
|
let mut events2 = self.dev.as_ref().unwrap().notifications().await.unwrap();
|
||||||
while let Some(event) = events2.next().await {
|
while let Some(event) = events2.next().await {
|
||||||
self.ack().await.unwrap();
|
self.ack().await.unwrap();
|
||||||
@ -218,6 +227,7 @@ impl Miband8 {
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn handle_watch_nonce(&mut self, notification_value: &Vec<u8>) -> Result<Vec<u8>, Box<dyn Error>> {
|
async fn handle_watch_nonce(&mut self, notification_value: &Vec<u8>) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
let watch_nonce: Vec<u8> = notification_value[15..31].to_vec();
|
let watch_nonce: Vec<u8> = notification_value[15..31].to_vec();
|
||||||
let watch_hmac: Vec<u8> = notification_value[33..65].to_vec();
|
let watch_hmac: Vec<u8> = notification_value[33..65].to_vec();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user