diff --git a/src-tauri/src/async_bridge.rs b/src-tauri/src/async_bridge.rs new file mode 100644 index 0000000..f171132 --- /dev/null +++ b/src-tauri/src/async_bridge.rs @@ -0,0 +1,7 @@ +mod bridge; +mod command; +mod response; + +pub use bridge::*; +pub use command::*; +pub use response::*; diff --git a/src-tauri/src/async_bridge/bridge.rs b/src-tauri/src/async_bridge/bridge.rs new file mode 100644 index 0000000..18634c7 --- /dev/null +++ b/src-tauri/src/async_bridge/bridge.rs @@ -0,0 +1,161 @@ +use std::sync::Arc; + +use tauri::AppHandle; +use tokio::sync::{mpsc, Mutex}; + +use soundcore_lib::device::SoundcoreBLEDevice; +use soundcore_lib::{ + ble::BLEConnectionManager, + device_manager::{create_device_manager, DeviceManager}, +}; + +use super::{BridgeCommand, BridgeResponse}; + +struct CommandLoopState { + manager: DeviceManager, + app_handle: AppHandle, + devices: Vec>>, +} + +impl CommandLoopState { + fn new(manager: DeviceManager, app_handle: AppHandle) -> Self { + Self { + manager, + app_handle, + devices: Vec::new(), + } + } +} + +pub async fn async_bridge( + mut input_rx: mpsc::Receiver, + output_tx: mpsc::Sender, + app_handle: AppHandle, +) { + let command_loop = tokio::spawn(async move { + let manager = create_device_manager().await; + let command_loop_state = Arc::new(Mutex::new(CommandLoopState::new(manager, app_handle))); + loop { + while let Some(command) = input_rx.recv().await { + let command_loop_state = command_loop_state.clone(); + let response = handle_command(command_loop_state, command, output_tx.clone()).await; + output_tx + .send(response) + .await + .expect("Failed to send response"); + } + } + }); +} + +async fn handle_command( + command_loop_state: Arc>>, + command: BridgeCommand, + output_tx: mpsc::Sender, +) -> BridgeResponse { + match command { + BridgeCommand::ScanBle => command_loop_state + .lock() + .await + .manager + .ble_scan(None) + .await + .map(BridgeResponse::ScanResult), + BridgeCommand::DisconnectBle(addr) => { + let addr_clone = addr.clone(); + command_loop_state + .lock() + .await + .manager + .disconnect(addr) + .await + .map(|_| BridgeResponse::Disconnected(addr_clone)) + } + BridgeCommand::ConnectBle(d) => { + let addr = d.clone().descriptor.addr; + let device = command_loop_state.lock().await.manager.connect(d).await; + let addr_clone = addr.clone(); + if let Ok(device) = device { + // Get the state channel and listen for changes in the background + let mut state_channel = device.state_channel().await; + tokio::task::spawn(async move { + while let Ok(()) = state_channel.changed().await { + let state = state_channel.borrow().clone(); + // TODO: Add logging + output_tx + .send(BridgeResponse::NewState((addr_clone.clone(), state))) + .await; + } + }); + Ok(BridgeResponse::ConnectionEstablished(addr)) + } else { + Err(device.err().unwrap()) + } + } + } + .map_err(|e| BridgeResponse::Error(e.to_string())) + .unwrap_or_else(|e| e) +} + +// #[cfg(test)] +// mod test { +// use super::*; +// +// async fn create_bridge() -> (mpsc::Sender, mpsc::Receiver) { +// let (input_tx, input_rx) = mpsc::channel(1); +// let (output_tx, output_rx) = mpsc::channel(1); +// async_bridge(input_rx, output_tx).await; +// (input_tx, output_rx) +// } +// +// #[tokio::test] +// async fn should_handle_scan_command_and_produce_response() { +// let (input_tx, mut output_rx) = create_bridge().await; +// input_tx +// .send(BridgeCommand::ScanBle) +// .await +// .expect("Failed to send command"); +// +// let response = output_rx.recv().await.expect("Failed to receive response"); +// +// match response { +// BridgeResponse::ScanResult(res) => { +// assert!(!res.is_empty()); +// } +// _ => panic!("Unexpected response: {:?}", response), +// } +// } +// +// #[tokio::test] +// async fn should_handle_connect_command_and_produce_response() { +// let (input_tx, mut output_rx) = create_bridge().await; +// input_tx +// .send(BridgeCommand::ScanBle) +// .await +// .expect("Failed to send command"); +// +// let scan_response = output_rx.recv().await.expect("Failed to receive response"); +// +// let devices = match scan_response { +// BridgeResponse::ScanResult(res) => res, +// _ => panic!("Unexpected response: {:?}", scan_response), +// }; +// +// let device = devices.first().unwrap(); +// +// input_tx +// .send(BridgeCommand::ConnectBle(device.clone())) +// .await +// .expect("Failed to send command"); +// +// let response = output_rx.recv().await.expect("Failed to receive response"); +// +// // TODO: Fix this test +// // match response { +// // BridgeResponse::ConnectionEstablished(addr) => { +// // assert_eq!(addr, device.descriptor.addr); +// // } +// // _ => panic!("Unexpected response: {:?}", response), +// // } +// } +// } diff --git a/src-tauri/src/async_bridge/command.rs b/src-tauri/src/async_bridge/command.rs new file mode 100644 index 0000000..3a27371 --- /dev/null +++ b/src-tauri/src/async_bridge/command.rs @@ -0,0 +1,14 @@ +use serde::Deserialize; +use typeshare::typeshare; +use soundcore_lib::btaddr::BluetoothAdrr; + +use soundcore_lib::device_manager::DiscoveredDevice; + +#[typeshare] +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase", tag = "command", content = "payload")] +pub enum BridgeCommand { + ScanBle, + ConnectBle(DiscoveredDevice), + DisconnectBle(BluetoothAdrr), +} diff --git a/src-tauri/src/async_bridge/response.rs b/src-tauri/src/async_bridge/response.rs new file mode 100644 index 0000000..2a32250 --- /dev/null +++ b/src-tauri/src/async_bridge/response.rs @@ -0,0 +1,17 @@ +use serde::Serialize; + +use soundcore_lib::api::SoundcoreDeviceState; +use soundcore_lib::btaddr::BluetoothAdrr; +use soundcore_lib::device_manager::DiscoveredDevice; + +#[derive(Debug, Serialize, Clone)] +pub enum BridgeResponse { + ScanResult(Vec), + ConnectionEstablished(BluetoothAdrr), + NewState((BluetoothAdrr, SoundcoreDeviceState)), + Disconnected(BluetoothAdrr), + Error(String), +} + +unsafe impl Send for BridgeResponse {} +unsafe impl Sync for BridgeResponse {} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f413c0d..ad370e1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,17 +3,24 @@ windows_subsystem = "windows" )] +use std::sync::Arc; + +use log::info; +use mpsc::channel; +use tauri::async_runtime::{Mutex, RwLock}; +use tauri::Manager; +use tauri_plugin_log::LogTarget; +use tokio::sync::mpsc; + use bluetooth_lib::platform::BthScanner; use bluetooth_lib::Scanner; use frontend_types::BthScanResult; use soundcore_lib::base::SoundcoreDevice; -use soundcore_lib::types::{SupportedModels, SOUNDCORE_NAME_MODEL_MAP}; +use soundcore_lib::types::{SOUNDCORE_NAME_MODEL_MAP, SupportedModels}; -use std::sync::Arc; -use tauri::async_runtime::{Mutex, RwLock}; -use tauri::Manager; -use tauri_plugin_log::LogTarget; +use crate::async_bridge::{async_bridge, BridgeCommand, BridgeResponse}; +pub(crate) mod async_bridge; mod device; pub(crate) mod frontend_types; mod tray; @@ -22,15 +29,6 @@ pub(crate) mod utils; #[cfg(target_os = "macos")] mod server; -// #[tauri::command] -// fn close_all(state: State) -> Result<(), ()> { -// let mut device_state = state.device.lock().map_err(|_| ())?; -// *device_state = None; -// let rfcomm = RFCOMM_STATE.lock().map_err(|_| ())?; -// rfcomm.close(); -// Ok(()) -// } - #[tauri::command] async fn scan_for_devices() -> Vec { let res = BthScanner::new().scan().await; @@ -51,25 +49,47 @@ async fn scan_for_devices() -> Vec { struct SoundcoreAppState { device: Arc>>>, model: Arc>>, + bridge_tx: Mutex>, } -fn main() { +#[tokio::main] +async fn main() { + tauri::async_runtime::set(tokio::runtime::Handle::current()); #[cfg(target_os = "macos")] server::launch_server(); + // Bring up bridge + // builder() // .filter(None, log::LevelFilter::Debug) // .filter_module("h2", log::LevelFilter::Off) // .filter_module("hyper", log::LevelFilter::Off) // .filter_module("tower", log::LevelFilter::Off) // .init(); + let (input_tx, input_rx) = channel(1); + let (output_tx, mut output_rx) = channel(1); tauri::Builder::default() + .setup(|app| { + let bridge_app_handle = app.handle(); + tokio::spawn(async_bridge(input_rx, output_tx, bridge_app_handle)); + + let app_handle = app.handle(); + tokio::spawn(async move { + loop { + if let Some(resp) = output_rx.recv().await { + handle_bridge_output(resp, &app_handle); + } + } + }); + Ok(()) + }) .system_tray(tray::get_system_tray()) .on_system_tray_event(tray::handle_tray_event) .manage(SoundcoreAppState { device: Arc::new(Mutex::new(None)), model: Arc::new(RwLock::new(None)), + bridge_tx: Mutex::new(input_tx), }) .invoke_handler(tauri::generate_handler![ tray::set_tray_device_status, @@ -87,6 +107,7 @@ fn main() { device::set_eq, device::get_eq, scan_for_devices, + send_bridge_command ]) .plugin(tauri_plugin_log::Builder::default().targets([ LogTarget::LogDir, @@ -120,3 +141,17 @@ fn main() { _ => {} }); } + +fn handle_bridge_output(resp: BridgeResponse, manager: &impl Manager) { + info!("Received response from bridge: {:?}", resp); + manager.emit_all("bridge-response", resp).unwrap(); +} + +#[tauri::command] +async fn send_bridge_command( + app_state: tauri::State<'_, SoundcoreAppState>, + command: BridgeCommand, +) -> Result<(), String> { + let tx = app_state.bridge_tx.lock().await; + tx.send(command).await.map_err(|e| e.to_string()) +}