Skip to content

Commit

Permalink
feat: Historical navigation within one characteristic (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmtrKovalenko authored Jul 29, 2023
1 parent 00d0467 commit f84b1a6
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 66 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"Blendr"
"Blendr",
"Keydown"
]
}
56 changes: 52 additions & 4 deletions src/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ use crate::{
use btleplug::api::Peripheral;
use std::{
ops::{Deref, DerefMut},
sync::{atomic::AtomicU16, Arc, RwLock},
sync::{
atomic::{AtomicIsize, AtomicU16},
Arc, RwLock,
},
time::Duration,
};
use tokio::time::{self, timeout};
Expand All @@ -19,6 +22,46 @@ pub struct CharacteristicValue {
pub data: Vec<u8>,
}

/// Atomic implementation for optional index
#[derive(Debug)]
pub struct AtomicOptionalIndex(AtomicIsize);

impl Default for AtomicOptionalIndex {
fn default() -> Self {
Self(AtomicIsize::new(-1))
}
}

impl AtomicOptionalIndex {
pub fn read(&self) -> Option<usize> {
let value = self.0.load(std::sync::atomic::Ordering::SeqCst);

if value < 0 {
None
} else {
Some(value as usize)
}
}

pub fn write(&self, value: usize) {
let new_value: isize = if let Ok(new_value) = value.try_into() {
new_value
} else {
tracing::error!(
"Failed to convert atomic optional index. Falling back to the isize max"
);

isize::MAX
};

self.0.store(new_value, std::sync::atomic::Ordering::SeqCst)
}

pub fn annulate(&self) {
self.0.store(-1, std::sync::atomic::Ordering::SeqCst)
}
}

#[derive(Debug, Clone)]
pub enum Route {
PeripheralList,
Expand All @@ -27,10 +70,14 @@ pub enum Route {
peripheral: HandledPeripheral,
retry: Arc<AtomicU16>,
},
// todo pull out into separate struct with default impl
CharacteristicView {
peripheral: ConnectedPeripheral,
characteristic: ConnectedCharacteristic,
value: Arc<RwLock<Option<CharacteristicValue>>>,
/// If index is negative should show the latest mode,
/// can't use option here cause it will require additional mutex lock around while we can use simple atomic here
historical_view_index: Arc<AtomicOptionalIndex>,
history: Arc<RwLock<Vec<CharacteristicValue>>>,
},
}

Expand Down Expand Up @@ -74,15 +121,16 @@ impl Route {
Route::CharacteristicView {
peripheral,
characteristic,
value,
history,
..
},
) => loop {
let ble_peripheral = &peripheral.peripheral.ble_peripheral;
if let Ok(data) = ble_peripheral
.read(&characteristic.ble_characteristic)
.await
{
value.write().unwrap().replace(CharacteristicValue {
history.write().unwrap().push(CharacteristicValue {
time: chrono::Local::now(),
data,
});
Expand Down
177 changes: 144 additions & 33 deletions src/tui/connection_view.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use crate::{
bluetooth::display_properties,
route::Route,
bluetooth::ConnectedCharacteristic,
route::{CharacteristicValue, Route},
tui::{
ui::{block, BlendrBlock},
AppRoute,
ui::{
block::{self, Title},
BlendrBlock,
},
AppRoute, HandleKeydownResult,
},
Ctx,
};
Expand Down Expand Up @@ -52,6 +55,54 @@ fn try_parse_numeric_value<T: ByteOrder>(
})
}

fn render_title_with_navigation_controls(
area: &tui::layout::Rect,
char: &ConnectedCharacteristic,
historical_view_index: Option<usize>,
history: &[CharacteristicValue],
) -> Title<'static> {
const PREVIOUS_BUTTON: &str = " [<- Previous] ";
const PREVIOUS_BUTTON_DENSE: &str = " [<-] ";
const NEXT_BUTTON: &str = " [Next ->] ";
const NEXT_BUTTON_DENSE: &str = " [->] ";

let mut spans = vec![];
let available_width = area.width - 2; // 2 chars for borders on the left and right
let base_title = format!(
"Characteristic {} / Service {}",
char.char_name(),
char.service_name()
);

let previous_button_style = if history.len() < 2 || historical_view_index == Some(0) {
Style::default().fg(Color::DarkGray)
} else {
Style::default()
};

let next_button_style = if history.len() < 2 || historical_view_index.is_none() {
Style::default().fg(Color::DarkGray)
} else {
Style::default()
};

let not_enough_space = (available_width as i32).saturating_sub(
PREVIOUS_BUTTON.len() as i32 + NEXT_BUTTON.len() as i32 + base_title.len() as i32,
) < 0;

if not_enough_space {
spans.push(Span::styled(PREVIOUS_BUTTON_DENSE, previous_button_style));
spans.push(Span::raw(format!("Char. {}", char.char_name())));
spans.push(Span::styled(NEXT_BUTTON_DENSE, next_button_style));
} else {
spans.push(Span::styled(PREVIOUS_BUTTON, previous_button_style));
spans.push(Span::raw(base_title));
spans.push(Span::styled(NEXT_BUTTON, next_button_style));
}

Title::new(spans)
}

impl AppRoute for ConnectionView {
fn new(ctx: std::sync::Arc<crate::Ctx>) -> Self
where
Expand All @@ -67,21 +118,63 @@ impl AppRoute for ConnectionView {
}
}

fn handle_input(&mut self, key: &crossterm::event::KeyEvent) {
fn handle_input(&mut self, key: &crossterm::event::KeyEvent) -> HandleKeydownResult {
match key.code {
KeyCode::Char('f') => {
self.float_numbers = !self.float_numbers;
return;
return HandleKeydownResult::Handled;
}
KeyCode::Char('u') => {
self.unsigned_numbers = !self.unsigned_numbers;
return;
return HandleKeydownResult::Handled;
}
_ => (),
}

let active_route = self.ctx.get_active_route();

if let Route::CharacteristicView {
historical_view_index,
history,
..
} = active_route.deref()
{
let update_index = |new_index| {
historical_view_index.write(new_index);
};

match (
key.code,
history.read().ok().as_ref(),
historical_view_index.deref().read(),
) {
(KeyCode::Left, _, Some(current_historical_index)) => {
if current_historical_index >= 1 {
update_index(current_historical_index - 1);
}
}
(KeyCode::Left, Some(history), None) => {
update_index(history.len() - 1);
}
(KeyCode::Right, Some(history), Some(current_historical_index))
if current_historical_index == history.len() - 2 =>
{
historical_view_index.annulate();
}
(KeyCode::Right, Some(history), Some(current_historical_index)) => {
if history.len() > current_historical_index {
update_index(current_historical_index + 1);
}
}
_ => (),
}

if matches!(key.code, KeyCode::Left | KeyCode::Right) {
// on this view we always handing arrows as history navigation and preventing other view's actions
return HandleKeydownResult::Handled;
}
}

match (active_route.deref(), self.clipboard.as_mut()) {
(Route::CharacteristicView { characteristic, .. }, Some(clipboard)) => match key.code {
KeyCode::Char('c') => {
Expand All @@ -96,6 +189,8 @@ impl AppRoute for ConnectionView {
},
_ => (),
}

HandleKeydownResult::Continue
}

fn render(
Expand All @@ -105,35 +200,47 @@ impl AppRoute for ConnectionView {
f: &mut tui::Frame<super::TerminalBackend>,
) -> crate::error::Result<()> {
let active_route = self.ctx.active_route.read()?;
let (_, characteristic, value) = if let Route::CharacteristicView {
peripheral,
characteristic,
value,
} = active_route.deref()
{
(peripheral, characteristic, value)
} else {
tracing::error!(
"ConnectionView::render called when active route is not CharacteristicView"
);
let (_, characteristic, history, historical_view_index) =
if let Route::CharacteristicView {
peripheral,
characteristic,
history,
historical_view_index,
} = active_route.deref()
{
(peripheral, characteristic, history, historical_view_index)
} else {
tracing::error!(
"ConnectionView::render called when active route is not CharacteristicView"
);

return Ok(());
};
return Ok(());
};

let mut text = vec![];
text.push(Line::from(""));
let history = history.read()?;
let historical_index = historical_view_index.deref().read();

text.push(Line::from(format!(
"Properties: {}",
display_properties(characteristic.ble_characteristic.properties)
)));
let active_value = match historical_index {
Some(index) => history.get(index),
None => history.last(),
};

if let Some(value) = value.read().unwrap().as_ref() {
let mut text = vec![];
if let Some(value) = active_value.as_ref() {
text.push(Line::from(""));

text.push(Line::from(format!(
"Last updated: {}",
value.time.format("%Y-%m-%d %H:%M:%S")
"{label}: {}",
value.time.format("%Y-%m-%d %H:%M:%S"),
label = if let Some(index) = historical_index {
format!(
"Historical data ({} of {}) viewing data of\n",
index + 1,
history.len()
)
} else {
"Latest value received".to_owned()
},
)));

text.push(Line::from(""));
Expand Down Expand Up @@ -210,10 +317,12 @@ impl AppRoute for ConnectionView {
.block(tui::widgets::Block::from(BlendrBlock {
route_active,
focused: route_active,
title: format!(
"Characteristic {} / Service {}",
characteristic.char_name(),
characteristic.service_name()
title_alignment: tui::layout::Alignment::Center,
title: render_title_with_navigation_controls(
&area,
characteristic,
historical_index,
&history,
),
..Default::default()
}));
Expand All @@ -222,6 +331,8 @@ impl AppRoute for ConnectionView {
if chunks[1].height > 0 {
f.render_widget(
block::render_help([
Some(("<-", "Previous value", false)),
Some(("->", "Next value", false)),
Some(("d", "Disconnect from device", false)),
Some(("u", "Parse numeric as unsigned", self.unsigned_numbers)),
Some(("f", "Parse numeric as floats", self.float_numbers)),
Expand Down
12 changes: 7 additions & 5 deletions src/tui/error_popup.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
error,
tui::{ui::BlendrBlock, AppRoute, TerminalBackend},
tui::{ui::BlendrBlock, AppRoute, HandleKeydownResult, TerminalBackend},
Ctx,
};
use crossterm::event::KeyCode;
Expand Down Expand Up @@ -51,18 +51,19 @@ impl AppRoute for ErrorView {
ErrorView { ctx }
}

fn handle_input(&mut self, key: &crossterm::event::KeyEvent) {
fn handle_input(&mut self, key: &crossterm::event::KeyEvent) -> HandleKeydownResult {
// unwrap here because we are already in error state and if can not get out of it – it means a super serious race condition
let mut global_error_lock = self.ctx.global_error.lock().unwrap();
if global_error_lock.deref().is_none() {
return;
return HandleKeydownResult::Errored;
}

match key.code {
KeyCode::Esc | KeyCode::Enter | KeyCode::Tab | KeyCode::Char(' ') => {
*global_error_lock.deref_mut() = None
*global_error_lock.deref_mut() = None;
HandleKeydownResult::Handled
}
_ => {}
_ => HandleKeydownResult::Continue,
}
}

Expand All @@ -88,6 +89,7 @@ impl AppRoute for ErrorView {
route_active: true,
title: "Error",
color: Some(tui::style::Color::Red),
..Default::default()
}),
);

Expand Down
Loading

0 comments on commit f84b1a6

Please sign in to comment.