add plot renderer and config options

This commit is contained in:
KubaWis 2026-02-12 19:16:36 +01:00
parent 20987a66b4
commit cc60f6990e
No known key found for this signature in database
7 changed files with 274 additions and 40 deletions

View File

@ -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
View 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
View 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
View 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
View 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(())
}

View File

@ -1,45 +1,76 @@
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 &param_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)],
@ -49,6 +80,62 @@ async fn main() {
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");
}
} }

View File

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