Skip to content

Commit

Permalink
Add CSV export functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
ztroop committed Aug 29, 2024
1 parent 61e3522 commit 92b5e5b
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 23 deletions.
25 changes: 24 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "btlescan"
version = "0.1.1"
version = "0.1.2"
edition = "2021"

[dependencies]
Expand All @@ -12,3 +12,5 @@ futures = "0.3"
chrono = "0.4"
uuid = "1.6"
lazy_static = "1.4.0"
csv = "1.3"
serde = { version = "1.0", features = ["derive"] }
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This tool provides a cross-platform CLI with an interactive way to view Bluetoot
- **Up/Down Arrows**: Scroll through the list of devices.
- **Q**: Quit the application.
- **S**: Toggle scanning.
- **C**: Export CSV data to current directory.
- **ENTER**: Open or close widget.

## Installation
Expand Down
31 changes: 27 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
use std::{
error::Error,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};

use ratatui::widgets::TableState;
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};

use crate::{
scan::{bluetooth_scan, get_characteristics},
structs::{Characteristic, DeviceInfo},
structs::{Characteristic, DeviceCsv, DeviceInfo},
};

pub enum DeviceData {
Expand All @@ -18,6 +21,7 @@ pub enum DeviceData {
Error(String),
}

#[allow(dead_code)]
pub struct App {
pub rx: UnboundedReceiver<DeviceData>,
pub tx: UnboundedSender<DeviceData>,
Expand Down Expand Up @@ -73,4 +77,23 @@ impl App {

tokio::spawn(async move { get_characteristics(tx_clone, device).await });
}

pub fn get_devices_csv(&self) -> Result<(), Box<dyn Error>> {
let now = chrono::Local::now();
let timestamp = now.format("%Y-%m-%d_%H-%M-%S").to_string();
let file_path = format!("btlescan_{}.csv", timestamp);
let file = std::fs::File::create(file_path).expect("Unable to create file");
let mut wtr = csv::Writer::from_writer(file);
for device in &self.devices {
wtr.serialize(DeviceCsv {
id: device.id.clone(),
name: device.name.clone(),
tx_power: device.tx_power.clone(),
address: device.address.clone(),
rssi: device.rssi.clone(),
})?;
}
wtr.flush()?;
Ok(())
}
}
12 changes: 12 additions & 0 deletions src/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use uuid::Uuid;

/// A struct to hold the information of a Bluetooth device.
#[derive(Clone, Default)]
#[allow(dead_code)]
pub struct DeviceInfo {
pub id: String,
pub name: String,
Expand All @@ -14,6 +15,7 @@ pub struct DeviceInfo {
pub manufacturer_data: HashMap<u16, Vec<u8>>,
pub services: Vec<Uuid>,
pub detected_at: String,

pub service_data: HashMap<Uuid, Vec<u8>>,
pub device: Option<btleplug::platform::Peripheral>,
}
Expand Down Expand Up @@ -69,3 +71,13 @@ pub struct ManufacturerData {
pub company_code: String,
pub data: String,
}

/// A struct to hold data for a CSV file.
#[derive(serde::Serialize)]
pub struct DeviceCsv {
pub id: String,
pub name: String,
pub tx_power: String,
pub address: String,
pub rssi: String,
}
6 changes: 6 additions & 0 deletions src/viewer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ pub async fn viewer<B: Backend>(
let current_state = app.pause_status.load(Ordering::SeqCst);
app.pause_status.store(!current_state, Ordering::SeqCst);
}
KeyCode::Char('c') => {
let _ = app.get_devices_csv();
app.error_message =
"Devices exported to a CSV file in the current directory.".to_string();
app.error_view = true;
}
KeyCode::Enter => {
if app.error_view {
app.error_view = false;
Expand Down
24 changes: 7 additions & 17 deletions src/widgets/info_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,19 @@ use ratatui::{
pub fn info_table(signal: bool, is_loading: &bool, frame_count: &usize) -> Table<'static> {
let spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let index = frame_count % spinner.len();
let info_rows = vec![Row::new(vec![
"[q → exit]".to_string(),
"[up/down → navigate]".to_string(),
"[enter → open/close]".to_string(),
let info_text = format!(
"[q → exit] [c → csv] [up/down → navigate] [enter → open/close] {}",
if *is_loading {
format!("[loading... {}]", spinner[index])
} else if signal {
"[s → start scan]".to_string()
} else {
"[s → stop scan]".to_string()
},
])
.style(Style::default().fg(Color::DarkGray))];
let table = Table::new(
info_rows,
[
Constraint::Length(10),
Constraint::Length(20),
Constraint::Length(20),
Constraint::Length(20),
],
)
.column_spacing(1);
}
);

let info_row = vec![Row::new(vec![info_text]).style(Style::default().fg(Color::DarkGray))];
let table = Table::new(info_row, [Constraint::Fill(1)]).column_spacing(1);

table
}

0 comments on commit 92b5e5b

Please sign in to comment.