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"
|
||||
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"
|
||||
|
||||
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(())
|
||||
}
|
||||
161
src/main.rs
161
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::<u8>(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::<Vec<String>>();
|
||||
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<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::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<Sha256>;
|
||||
type Aes128Ccm = Ccm<Aes128, U4, U12>;
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Miband8 {
|
||||
dev: Option<Peripheral>,
|
||||
central: Option<Adapter>,
|
||||
@ -195,13 +198,19 @@ impl Miband8 {
|
||||
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 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>) -> 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<u8>) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
let watch_nonce: Vec<u8> = notification_value[15..31].to_vec();
|
||||
let watch_hmac: Vec<u8> = notification_value[33..65].to_vec();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user