project initiate.
This commit is contained in:
134
src-tauri/src/bluetooth.rs
Normal file
134
src-tauri/src/bluetooth.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use btleplug::{
|
||||
api::{Central, CentralState, Manager as _, Peripheral as _, ScanFilter},
|
||||
platform::{Adapter, Manager, Peripheral},
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use tauri::{
|
||||
async_runtime::{self, JoinHandle, RwLock},
|
||||
AppHandle, Emitter, State,
|
||||
};
|
||||
|
||||
use crate::{errors, state::AppState};
|
||||
|
||||
pub async fn handle_bluetooth_events(
|
||||
app_handle: Arc<AppHandle>,
|
||||
app_state: Arc<RwLock<AppState>>,
|
||||
) -> Result<JoinHandle<()>, errors::AppError> {
|
||||
let state = app_state.read().await;
|
||||
let adapter = state.central_adapter.read().await;
|
||||
let app = Arc::clone(&app_handle);
|
||||
if let Some(adapter) = &*adapter {
|
||||
let adapter = adapter.clone();
|
||||
let app_state = Arc::clone(&app_state);
|
||||
Ok(async_runtime::spawn(async move {
|
||||
let mut event_stream = adapter.events().await.unwrap();
|
||||
while let Some(event) = event_stream.next().await {
|
||||
match event {
|
||||
btleplug::api::CentralEvent::DeviceDiscovered(_id) => {
|
||||
app.emit("app_state_updated", ()).unwrap();
|
||||
}
|
||||
btleplug::api::CentralEvent::DeviceConnected(_id) => {
|
||||
let state = app_state.write().await;
|
||||
let peripherals = adapter.peripherals().await;
|
||||
if let Ok(peripherals) = peripherals {
|
||||
for peripheral in peripherals {
|
||||
if peripheral.id() == _id {
|
||||
state.set_connected_peripheral(peripheral).await;
|
||||
app.emit("app_state_updated", ()).unwrap();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
btleplug::api::CentralEvent::DeviceDisconnected(_id) => {
|
||||
let state = app_state.write().await;
|
||||
state.clear_connected_peripheral().await;
|
||||
app.emit("app_state_updated", ()).unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
app.emit("app_state_updated", ()).unwrap();
|
||||
Err(errors::AppError::BluetoothNotReady)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_central_adapter() -> Result<Adapter, errors::AppError> {
|
||||
let manager = Manager::new()
|
||||
.await
|
||||
.map_err(|_| errors::AppError::BluetoothNotReady)?;
|
||||
let adapters = manager
|
||||
.adapters()
|
||||
.await
|
||||
.map_err(|_| errors::AppError::BluetoothAdapterNotFound)?;
|
||||
|
||||
let mut found_adapter = None;
|
||||
for adpater in adapters {
|
||||
let state = adpater.adapter_state().await;
|
||||
if let Ok(state) = state {
|
||||
if state == CentralState::PoweredOn {
|
||||
found_adapter = Some(adpater);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
found_adapter.ok_or(errors::AppError::NoAvailableBluetoothAdapter)
|
||||
}
|
||||
|
||||
pub async fn start_scan(
|
||||
app_handle: Arc<AppHandle>,
|
||||
app_state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<(), errors::AppError> {
|
||||
let state = app_state.read().await;
|
||||
let adapter = state.get_central_adapter().await;
|
||||
if let Some(adapter) = adapter {
|
||||
adapter
|
||||
.start_scan(ScanFilter::default())
|
||||
.await
|
||||
.map_err(|_| errors::AppError::UnableToStartScan)?;
|
||||
app_handle.emit("app_state_updated", ()).unwrap();
|
||||
state.set_scanning(true).await;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors::AppError::NoAvailableBluetoothAdapter)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop_scan(
|
||||
app_handle: Arc<AppHandle>,
|
||||
app_state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<(), errors::AppError> {
|
||||
let state = app_state.read().await;
|
||||
let adapter = state.get_central_adapter().await;
|
||||
if let Some(adapter) = adapter {
|
||||
adapter
|
||||
.stop_scan()
|
||||
.await
|
||||
.map_err(|_| errors::AppError::UnableToStopScan)?;
|
||||
app_handle.emit("app_state_updated", ()).unwrap();
|
||||
state.set_scanning(false).await;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors::AppError::NoAvailableBluetoothAdapter)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_peripherals(
|
||||
app_state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<Vec<Peripheral>, errors::AppError> {
|
||||
let state = app_state.read().await; // Change from lock() to read()
|
||||
let adapter = state.get_central_adapter().await;
|
||||
if let Some(adapter) = adapter {
|
||||
Ok(adapter
|
||||
.peripherals()
|
||||
.await
|
||||
.map_err(|_| errors::AppError::UnableToRetrievePeripherals)?)
|
||||
} else {
|
||||
Err(errors::AppError::NoAvailableBluetoothAdapter)
|
||||
}
|
||||
}
|
||||
122
src-tauri/src/cmd/mod.rs
Normal file
122
src-tauri/src/cmd/mod.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use btleplug::api::{Central as _, Peripheral as _};
|
||||
use state::{ApplicationState, CentralState, ChannelState, PeripheralItem};
|
||||
use tauri::{async_runtime::RwLock, AppHandle, Emitter, State};
|
||||
|
||||
use crate::{bluetooth, errors, state::AppState};
|
||||
|
||||
mod state;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn activate_central_adapter(
|
||||
app_handle: AppHandle,
|
||||
app_state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<(), errors::AppError> {
|
||||
let app = Arc::new(app_handle);
|
||||
let adapter = bluetooth::get_central_adapter().await?;
|
||||
{
|
||||
let state = app_state.read().await;
|
||||
state.set_central_adapter(adapter).await;
|
||||
state.clear_central_event_handler().await;
|
||||
}
|
||||
let handle =
|
||||
bluetooth::handle_bluetooth_events(Arc::clone(&app), Arc::clone(&app_state)).await?;
|
||||
{
|
||||
let state = app_state.write().await; // Changed from lock() to write()
|
||||
state.set_central_event_handler(handle).await;
|
||||
}
|
||||
app.emit("app_state_updated", ()).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn refresh_application_state(
|
||||
app_state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<ApplicationState, errors::AppError> {
|
||||
let state = app_state.read().await;
|
||||
let mut peripherals: Vec<PeripheralItem> = vec![];
|
||||
let mut connected_peripheral = None;
|
||||
|
||||
let central = state.get_central_adapter().await;
|
||||
let central_state = if let Some(central) = central {
|
||||
let central_device_state = central
|
||||
.adapter_state()
|
||||
.await
|
||||
.map_err(|_| errors::AppError::BluetoothNotReady)?;
|
||||
|
||||
let found_peripherals = central
|
||||
.peripherals()
|
||||
.await
|
||||
.map_err(|_| errors::AppError::UnableToRetrievePeripherals)?;
|
||||
for peripheral in found_peripherals {
|
||||
let properties = peripheral
|
||||
.properties()
|
||||
.await
|
||||
.map_err(|_| errors::AppError::UnableToRetrievePeripheralProperties)?;
|
||||
if let Some(properties) = properties {
|
||||
let represent = properties
|
||||
.local_name
|
||||
.unwrap_or_else(|| properties.address.to_string());
|
||||
let item = PeripheralItem {
|
||||
id: peripheral.id(),
|
||||
address: properties.address.to_string(),
|
||||
represent,
|
||||
is_connected: peripheral
|
||||
.is_connected()
|
||||
.await
|
||||
.map_err(|_| errors::AppError::UnableToRetrievePeripheralProperties)?,
|
||||
rssi: properties.rssi,
|
||||
battery: properties.tx_power_level,
|
||||
};
|
||||
if peripheral
|
||||
.is_connected()
|
||||
.await
|
||||
.map_err(|_| errors::AppError::UnableToRetrievePeripheralState)?
|
||||
{
|
||||
connected_peripheral = Some(item.clone());
|
||||
}
|
||||
peripherals.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
CentralState {
|
||||
is_ready: central_device_state == btleplug::api::CentralState::PoweredOn,
|
||||
is_scanning: *state.scanning.lock().await,
|
||||
connected: state
|
||||
.connected_peripheral
|
||||
.lock()
|
||||
.await
|
||||
.clone()
|
||||
.map(|p| p.id()),
|
||||
}
|
||||
} else {
|
||||
CentralState::default()
|
||||
};
|
||||
|
||||
Ok(ApplicationState {
|
||||
central: central_state,
|
||||
peripherals,
|
||||
connected_peripheral,
|
||||
channel_a: ChannelState::default(),
|
||||
channel_b: ChannelState::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_scan_devices(
|
||||
app_handle: AppHandle,
|
||||
app_state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<(), errors::AppError> {
|
||||
bluetooth::start_scan(Arc::new(app_handle), app_state).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_scan_devices(
|
||||
app_handle: AppHandle,
|
||||
app_state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<(), errors::AppError> {
|
||||
bluetooth::stop_scan(Arc::new(app_handle), app_state).await?;
|
||||
Ok(())
|
||||
}
|
||||
41
src-tauri/src/cmd/state.rs
Normal file
41
src-tauri/src/cmd/state.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use btleplug::platform::PeripheralId;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::playlist::PlayMode;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct CentralState {
|
||||
pub is_ready: bool,
|
||||
pub is_scanning: bool,
|
||||
pub connected: Option<PeripheralId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PeripheralItem {
|
||||
pub id: PeripheralId,
|
||||
pub address: String,
|
||||
pub represent: String,
|
||||
pub is_connected: bool,
|
||||
pub rssi: Option<i16>,
|
||||
pub battery: Option<i16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct ChannelState {
|
||||
pub is_playing: bool,
|
||||
pub strength: u32,
|
||||
pub strength_limit: u32,
|
||||
pub is_boosting: bool,
|
||||
pub boost_level: u32,
|
||||
pub boost_limit: u32,
|
||||
pub play_mode: PlayMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ApplicationState {
|
||||
pub central: CentralState,
|
||||
pub peripherals: Vec<PeripheralItem>,
|
||||
pub connected_peripheral: Option<PeripheralItem>,
|
||||
pub channel_a: ChannelState,
|
||||
pub channel_b: ChannelState,
|
||||
}
|
||||
23
src-tauri/src/config_db.rs
Normal file
23
src-tauri/src/config_db.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use std::{fs::DirBuilder, path::PathBuf};
|
||||
|
||||
use tauri::{path::PathResolver, AppHandle, Manager, Runtime};
|
||||
|
||||
fn ensure_dir<R: Runtime>(resolver: &PathResolver<R>) -> anyhow::Result<PathBuf> {
|
||||
let config_dir = resolver
|
||||
.local_data_dir()
|
||||
.map_err(|e| anyhow::anyhow!("Unable to get local data directory: {}", e))?;
|
||||
if !config_dir.exists() {
|
||||
let mut dir_creator = DirBuilder::new();
|
||||
dir_creator
|
||||
.recursive(true)
|
||||
.create(&config_dir)
|
||||
.map_err(|e| anyhow::anyhow!("Unable to create config directory: {}", e))?;
|
||||
}
|
||||
Ok(config_dir)
|
||||
}
|
||||
|
||||
pub fn open_config_db<R: Runtime>(app_handle: &AppHandle<R>) -> anyhow::Result<sled::Db> {
|
||||
let config_dir = ensure_dir(app_handle.path())?;
|
||||
let db_path = config_dir.join("conf.db");
|
||||
sled::open(&db_path).map_err(|e| anyhow::anyhow!("Unable to open config database: {}", e))
|
||||
}
|
||||
22
src-tauri/src/errors.rs
Normal file
22
src-tauri/src/errors.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error, Serialize)]
|
||||
pub enum AppError {
|
||||
#[error("Bluetooth not ready")]
|
||||
BluetoothNotReady,
|
||||
#[error("No Bluetooth adapter found")]
|
||||
BluetoothAdapterNotFound,
|
||||
#[error("No available Bluetooth adapter, maybe not powered on")]
|
||||
NoAvailableBluetoothAdapter,
|
||||
#[error("Unable to start scan devices")]
|
||||
UnableToStartScan,
|
||||
#[error("Unable to stop scan devices")]
|
||||
UnableToStopScan,
|
||||
#[error("Unable to retrieve peripherals")]
|
||||
UnableToRetrievePeripherals,
|
||||
#[error("Unable to retrieve peripheral properties")]
|
||||
UnableToRetrievePeripheralProperties,
|
||||
#[error("Unable to retrieve peripheral state")]
|
||||
UnableToRetrievePeripheralState,
|
||||
}
|
||||
63
src-tauri/src/lib.rs
Normal file
63
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
#![allow(dead_code)]
|
||||
#![feature(stmt_expr_attributes)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tauri::{
|
||||
async_runtime::{self, RwLock},
|
||||
generate_handler, Manager,
|
||||
};
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
mod bluetooth;
|
||||
mod cmd;
|
||||
mod config_db;
|
||||
mod errors;
|
||||
mod playlist;
|
||||
mod state;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[cfg(debug_assertions)]
|
||||
let devtools = tauri_plugin_devtools::init();
|
||||
|
||||
let mut builder = tauri::Builder::default();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
builder = builder.plugin(devtools);
|
||||
|
||||
builder
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_single_instance::init(|_app, _args, _cwd| {}))
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.setup(|app| {
|
||||
if let Err(e) = async_runtime::block_on(async {
|
||||
let state = state::AppState::new(app.handle()).await?;
|
||||
app.manage(Arc::new(RwLock::new(state)));
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}) {
|
||||
app.dialog()
|
||||
.message(e.to_string())
|
||||
.kind(tauri_plugin_dialog::MessageDialogKind::Error)
|
||||
.title("Initialization error")
|
||||
.blocking_show();
|
||||
return Err(e.into());
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
window.open_devtools();
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(generate_handler![
|
||||
cmd::refresh_application_state,
|
||||
cmd::activate_central_adapter,
|
||||
cmd::start_scan_devices,
|
||||
cmd::stop_scan_devices
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
6
src-tauri/src/main.rs
Normal file
6
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
estim_lib::run()
|
||||
}
|
||||
17
src-tauri/src/playlist.rs
Normal file
17
src-tauri/src/playlist.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PlayMode {
|
||||
#[serde(rename = "repeat")]
|
||||
Repeat,
|
||||
#[serde(rename = "repeat-one")]
|
||||
RepeatOne,
|
||||
#[serde(rename = "shuffle")]
|
||||
Shuffle,
|
||||
}
|
||||
|
||||
impl Default for PlayMode {
|
||||
fn default() -> Self {
|
||||
PlayMode::Repeat
|
||||
}
|
||||
}
|
||||
86
src-tauri/src/state.rs
Normal file
86
src-tauri/src/state.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use btleplug::platform::{Adapter, Peripheral};
|
||||
use tauri::{
|
||||
async_runtime::{JoinHandle, Mutex, RwLock},
|
||||
AppHandle, Runtime,
|
||||
};
|
||||
|
||||
use crate::config_db;
|
||||
|
||||
pub struct AppState {
|
||||
pub db: Arc<Mutex<sled::Db>>,
|
||||
pub central_adapter: Arc<RwLock<Option<Adapter>>>,
|
||||
pub central_event_handler: Arc<Mutex<Option<JoinHandle<()>>>>,
|
||||
pub scanning: Arc<Mutex<bool>>,
|
||||
pub connected_peripheral: Arc<Mutex<Option<Arc<Peripheral>>>>,
|
||||
}
|
||||
|
||||
unsafe impl Send for AppState {}
|
||||
unsafe impl Sync for AppState {}
|
||||
|
||||
impl AppState {
|
||||
pub async fn new<R: Runtime>(app: &AppHandle<R>) -> anyhow::Result<Self> {
|
||||
let db = config_db::open_config_db(app)?;
|
||||
|
||||
Ok(Self {
|
||||
db: Arc::new(Mutex::new(db)),
|
||||
central_adapter: Arc::new(RwLock::new(None)),
|
||||
central_event_handler: Arc::new(Mutex::new(None)),
|
||||
scanning: Arc::new(Mutex::new(false)),
|
||||
connected_peripheral: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set_central_adapter(&self, adapter: Adapter) {
|
||||
let mut central_adapter = self.central_adapter.write().await;
|
||||
*central_adapter = Some(adapter);
|
||||
}
|
||||
|
||||
pub async fn clear_central_adapter(&self) {
|
||||
let mut central_adapter = self.central_adapter.write().await;
|
||||
*central_adapter = None;
|
||||
}
|
||||
|
||||
pub async fn get_central_adapter(&self) -> Option<Adapter> {
|
||||
let central_adapter = self.central_adapter.read().await;
|
||||
central_adapter.clone()
|
||||
}
|
||||
|
||||
pub async fn set_central_event_handler(&self, handler: JoinHandle<()>) {
|
||||
let mut central_event_handler = self.central_event_handler.lock().await;
|
||||
*central_event_handler = Some(handler);
|
||||
}
|
||||
|
||||
pub async fn clear_central_event_handler(&self) {
|
||||
let mut central_event_handler = self.central_event_handler.lock().await;
|
||||
if let Some(handler) = central_event_handler.take() {
|
||||
handler.abort();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_scanning(&self, scanning_state: bool) {
|
||||
let mut scanning = self.scanning.lock().await;
|
||||
*scanning = scanning_state;
|
||||
}
|
||||
|
||||
pub async fn set_connected_peripheral(&self, peripheral: Peripheral) {
|
||||
let mut connected_peripheral = self.connected_peripheral.lock().await;
|
||||
*connected_peripheral = Some(Arc::new(peripheral));
|
||||
}
|
||||
|
||||
pub async fn clear_connected_peripheral(&self) {
|
||||
let mut connected_peripheral = self.connected_peripheral.lock().await;
|
||||
*connected_peripheral = None;
|
||||
}
|
||||
|
||||
pub async fn get_connected_peripheral(&self) -> Option<Arc<Peripheral>> {
|
||||
let connected_peripheral = self.connected_peripheral.lock().await;
|
||||
|
||||
if let Some(peripheral) = connected_peripheral.clone() {
|
||||
Some(Arc::clone(&peripheral))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user