Skip to content

Commit

Permalink
feat: Display all the history in the cli app
Browse files Browse the repository at this point in the history
  • Loading branch information
dmtrKovalenko committed May 21, 2024
1 parent 644adcc commit 3a55dbe
Show file tree
Hide file tree
Showing 11 changed files with 1,028 additions and 536 deletions.
644 changes: 478 additions & 166 deletions cli/Cargo.lock

Large diffs are not rendered by default.

13 changes: 5 additions & 8 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,22 @@ name = "cli"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
async-trait = "0.1.68"
btleplug = { version = "0.10", features = ["serde"] }
btleplug = { version = "0.11.5", features = ["serde"] }
chrono = "0.4.24"
crossterm = "0.26.1"
crossterm = "0.27.0"
futures = "0.3.28"
lazy_static = "1.4.0"
notify-rust = "4.8.0"
pretty_env_logger = "0.4.0"
pretty_env_logger = "0.5.0"
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
spinners = "4.1.0"
textplots = "0.8.0"
throbber-widgets-tui = "0.1.3"
tokio = { version = "1.28", features = ["full"] }
tracing = "0.1.37"
tracing-appender = "0.2.2"
tracing-subscriber = "0.3.17"
tui = "0.19.0"
uuid = "1.3.2"
ratatui = "0.26.3"
uuid = "1.3.2"
131 changes: 68 additions & 63 deletions cli/src/bluetooth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ const TIMEOUT: Duration = Duration::from_secs(10);
impl<TPer: Peripheral> Connection<TPer> {
pub async fn disconnect(&self) -> Result<(), Box<dyn Error>> {
tracing::debug!("Disconnecting from sensor");
self.peripheral.unsubscribe(&self.characteristic).await?;
self.peripheral.disconnect().await?;

tracing::debug!("Successsfuly disconnected from sensor");

Ok(())
}

Expand All @@ -39,8 +42,8 @@ impl<TPer: Peripheral> Connection<TPer> {
}
}

for _ in 1..10 {
const DISCONNECT_INTERVAL: Duration = Duration::from_secs(2);
for _ in 1..5 {
const DISCONNECT_INTERVAL: Duration = Duration::from_millis(500);

if let Err(e) = timeout(TIMEOUT, self.disconnect()).await {
tracing::error!(
Expand Down Expand Up @@ -70,9 +73,6 @@ impl<TPer: Peripheral> Connection<TPer> {
Err(e) => tracing::error!("Error decodring data from sensor {}", e),
}

sleep(Duration::from_millis(5000)).await;
tracing::debug!("Awake");

let is_connected = timeout(TIMEOUT, self.peripheral.is_connected())
.await
.map_err(|_| "Connection lost")??;
Expand All @@ -88,9 +88,7 @@ impl<TPer: Peripheral> Connection<TPer> {
pub async fn read_from_sensor<TData: FromBleData>(&self) -> Result<TData, Box<dyn Error>> {
tracing::debug!("Reading sensor");

Ok(TData::from_bytes(
self.peripheral.read(&self.characteristic).await?,
)?)
TData::from_bytes(self.peripheral.read(&self.characteristic).await?)
}
}

Expand All @@ -102,72 +100,79 @@ pub async fn find_sensor(
let manager = Manager::new().await?;
let adapter_list = manager.adapters().await?;
if adapter_list.is_empty() {
tracing::debug!("No Bluetooth adapters found");
panic!("No Bluetooth adapters found");
}

for adapter in adapter_list.iter() {
adapter.start_scan(ScanFilter::default()).await?;

time::sleep(Duration::from_secs(2)).await;
let peripherals = adapter.peripherals().await?;

if peripherals.is_empty() {
tracing::error!("No BLE peripherals found")
} else {
// All peripheral devices in range.
for peripheral in peripherals.into_iter() {
let properties = peripheral.properties().await?;
let adapter = adapter_list
.into_iter()
.next()
.expect("No Bluetooth adapters found");

adapter
.start_scan(ScanFilter {
services: vec![Uuid::from_u128(0x0000FFE0_0000_1000_8000_00805F9B34FB)],
})
.await?;

time::sleep(Duration::from_secs(2)).await;
let peripherals = adapter.peripherals().await?;

if peripherals.is_empty() {
tracing::error!("No BLE peripherals found")
} else {
// All peripheral devices in range.
for peripheral in peripherals.into_iter() {
let properties = peripheral.properties().await?;
let is_connected = peripheral.is_connected().await?;
let local_name = properties
.unwrap()
.local_name
.unwrap_or(String::from("(peripheral name unknown)"));
tracing::debug!("Connected to peripheral {:?}.", &local_name);

// Check if it's the peripheral we want.
if local_name.contains(name) {
if !is_connected {
// Connect if we aren't already connected.
if let Err(err) = peripheral.connect().await {
eprintln!("Error connecting to peripheral, skipping: {}", err);
continue;
}
}
let is_connected = peripheral.is_connected().await?;
let local_name = properties
.unwrap()
.local_name
.unwrap_or(String::from("(peripheral name unknown)"));
tracing::debug!("Connected to peripheral {:?}.", &local_name);

// Check if it's the peripheral we want.
if local_name.contains(name) {
if !is_connected {
// Connect if we aren't already connected.
if let Err(err) = peripheral.connect().await {
eprintln!("Error connecting to peripheral, skipping: {}", err);
continue;
tracing::debug!(
"Connected ({:?}) to peripheral {:?}.",
is_connected,
&local_name
);

if is_connected {
peripheral.discover_services().await?;

for characteristic in peripheral.characteristics().into_iter() {
if characteristic.uuid == characteristic_uuid
&& characteristic.properties.contains(property)
{
tracing::debug!("Found characteristic {:?}", characteristic.uuid,);
return Ok(Connection {
_manager: manager,
peripheral,
characteristic,
});
}
}
let is_connected = peripheral.is_connected().await?;

tracing::debug!(
"Connected ({:?}) to peripheral {:?}.",
is_connected,
"Peripheral {:?} does not have the required characteristic.",
&local_name
);

if is_connected {
peripheral.discover_services().await?;

for characteristic in peripheral.characteristics().into_iter() {
if characteristic.uuid == characteristic_uuid
&& characteristic.properties.contains(property)
{
tracing::debug!("Found characteristic {:?}", characteristic.uuid,);
return Ok(Connection {
_manager: manager,
peripheral,
characteristic,
});
}
}

tracing::debug!(
"Peripheral {:?} does not have the required characteristic.",
&local_name
);
peripheral.disconnect().await?;
}
peripheral.disconnect().await?;
}
}
}

adapter.stop_scan().await?;
}

adapter.stop_scan().await?;

Err("No devices found".into())
}
}
101 changes: 101 additions & 0 deletions cli/src/history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#![allow(dead_code)]
use crate::climate_data::ClimateData;

pub struct MaxSizedVector<T, const MAX_SIZE: usize> {
data: Vec<T>,
}

impl<T, const MAX_SIZE: usize> MaxSizedVector<T, MAX_SIZE> {
/// Constructs a new MaxSizedVector with a specified maximum size
pub fn new() -> Self {
Self {
data: Vec::with_capacity(MAX_SIZE),
}
}

/// Adds an element to the vector, removing the oldest if at max capacity
pub fn push(&mut self, item: T) {
if self.data.len() == MAX_SIZE {
self.data.remove(0);
}
self.data.push(item);
}

/// Returns a reference to the element at the given index if it exists
pub fn get(&self, index: usize) -> Option<&T> {
self.data.get(index)
}

/// Checks if the vector is at its maximum capacity
pub fn is_full(&self) -> bool {
self.data.len() == MAX_SIZE
}
/// Returns the number of elements currently in the vector
pub fn len(&self) -> usize {
self.data.len()
}

/// Removes all elements from the vector
pub fn clear(&mut self) {
self.data.clear();
}

/// Provides an iterator over the elements of the vector
pub fn iter(&self) -> std::slice::Iter<'_, T> {
self.data.iter()
}

/// Extracts a slice containing the entire vector.
pub fn as_slice(&self) -> &[T] {
self.data.as_slice()
}
}

// amount of 5 seconds intervals in 24 hours
const HISTORY_SIZE: usize = 17280;

/// .0 - timestamp, .1 - value
type HistoryPoint = (f64, f64);

pub struct History {
time_window: [f64; 2],
pub flat: MaxSizedVector<ClimateData, HISTORY_SIZE>,
pub co2_history: MaxSizedVector<HistoryPoint, HISTORY_SIZE>,
pub eco2_history: MaxSizedVector<HistoryPoint, HISTORY_SIZE>,
pub temperature_history: MaxSizedVector<HistoryPoint, HISTORY_SIZE>,
pub pressure_history: MaxSizedVector<HistoryPoint, HISTORY_SIZE>,
}

impl History {
pub fn get_window(&self) -> [f64; 2] {
self.time_window
}
pub fn new() -> Self {
let now = chrono::offset::Local::now().timestamp_millis() as f64;
Self {
time_window: [now, now],
flat: MaxSizedVector::new(),
co2_history: MaxSizedVector::new(),
eco2_history: MaxSizedVector::new(),
temperature_history: MaxSizedVector::new(),
pressure_history: MaxSizedVector::new(),
}
}

pub fn capture_measurement(&mut self, climate_data: &ClimateData) {
let now = chrono::offset::Local::now().timestamp_millis() as f64;
self.time_window[1] = now;

self.flat.push(*climate_data);
self.temperature_history
.push((now, climate_data.temperature as f64));

if let Some(co2) = climate_data.co2 {
self.co2_history.push((now, co2 as f64));
}

self.eco2_history.push((now, climate_data.eco2 as f64));
self.pressure_history
.push((now, climate_data.pressure as f64));
}
}
26 changes: 15 additions & 11 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
mod history;
mod tui_app;
use btleplug::api::CharPropFlags;
use climate_data::ClimateData;
use history::History;
use spinners::{Spinner, Spinners};
use tui_app::TerminalUi;
use uuid::Uuid;
mod bluetooth;
use ratatui::{backend::CrosstermBackend, Terminal};
use std::{error::Error, fmt::Display};
use tui::{backend::CrosstermBackend, Terminal};

mod climate_data;
mod reactions;
Expand All @@ -24,7 +26,7 @@ fn set_terminal_tab_title(climate_data: impl AsRef<str> + Display) {

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error>> {
let file_appender = tracing_appender::rolling::hourly(format!("/tmp/co2cicka"), "cli.log");
let file_appender = tracing_appender::rolling::hourly("/tmp/co2cicka", "cli.log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);

tracing_subscriber::fmt()
Expand All @@ -35,11 +37,12 @@ async fn main() -> Result<(), Box<dyn Error>> {

let stdout = std::io::stdout();
let backend = CrosstermBackend::new(stdout);
let mut history = History::new();
let mut app = TerminalUi::new()?;
let mut terminal = Terminal::new(backend)?;
let mut history = Vec::new();

loop {
let mut spinner_stopped = false;
let mut spinner = Spinner::new(Spinners::Dots9, "Connecting to sensor".to_owned());
tracing::debug!("Looking for a sensor...");
set_terminal_tab_title("Connecting to a sensor...");
Expand All @@ -51,8 +54,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
)
.await
{
let mut spinner_stopped = false;
match connection
let result = connection
.subscribe_to_sensor(|data: ClimateData| {
tracing::debug!("New climate data: {:?}", data);
if !spinner_stopped {
Expand All @@ -68,16 +70,18 @@ async fn main() -> Result<(), Box<dyn Error>> {
data.humidity.round()
));

history.capture_measurement(&data);

app.capture_measurements(&data);
app.draw(&mut terminal);
history.push(data);
app.draw(&history, &mut terminal);

if cfg!(debug_assertions) {
reactions::run_reactions(&history);
reactions::run_reactions(history.flat.as_slice());
}
})
.await
{
.await;

match result {
Ok(_) => {}
Err(e) => {
tracing::error!(error=?e, "Error while subscribing to sensor");
Expand All @@ -89,4 +93,4 @@ async fn main() -> Result<(), Box<dyn Error>> {

terminal.clear()?;
}
}
}
Loading

0 comments on commit 3a55dbe

Please sign in to comment.