From 9254c885776bb03ef6e2ec00e0f82828624cf7c9 Mon Sep 17 00:00:00 2001 From: SARDONYX-sard <68905624+SARDONYX-sard@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:06:44 +0900 Subject: [PATCH 1/6] refactor: separate to files --- crates/bluetooth/src/device/device_info.rs | 2 +- .../src/device/windows/address_parser.rs | 64 ++++++++++ .../src/device/windows/device_info/mod.rs | 32 ++--- .../src/device/windows/device_searcher.rs | 5 +- .../bluetooth/src/device/windows/inspect.rs | 57 +++++++++ crates/bluetooth/src/device/windows/mod.rs | 4 +- crates/bluetooth/src/device/windows/watch.rs | 120 ++++++------------ cspell.jsonc | 1 + gui/backend/src/cmd/device_watcher.rs | 11 +- 9 files changed, 187 insertions(+), 109 deletions(-) create mode 100644 crates/bluetooth/src/device/windows/address_parser.rs create mode 100644 crates/bluetooth/src/device/windows/inspect.rs diff --git a/crates/bluetooth/src/device/device_info.rs b/crates/bluetooth/src/device/device_info.rs index 99f266b..f09912f 100644 --- a/crates/bluetooth/src/device/device_info.rs +++ b/crates/bluetooth/src/device/device_info.rs @@ -70,7 +70,7 @@ impl LocalTime { } } - pub fn from_utc(utc_time: chrono::DateTime) -> Self { + pub fn from_utc(utc_time: &chrono::DateTime) -> Self { let time = utc_time.with_timezone(&chrono::Local); Self { year: time.year() as u16, diff --git a/crates/bluetooth/src/device/windows/address_parser.rs b/crates/bluetooth/src/device/windows/address_parser.rs new file mode 100644 index 0000000..a59eedb --- /dev/null +++ b/crates/bluetooth/src/device/windows/address_parser.rs @@ -0,0 +1,64 @@ +/// Convert address(`de:ad:be:ee:ee:ef`) of string (e.g., `Bluetooth#Bluetooth00:00:00:ff:ff:00-de:ad:be:ee:ee:ef`) into a [u64]. +pub fn id_to_address(id: &mut &str) -> winnow::PResult { + use winnow::prelude::Parser as _; + + let input = id; + let prefix = "Bluetooth#Bluetooth"; + let _ = (prefix, hex_address, '-').parse_next(input)?; + + // Convert address string (e.g., "00:00:00:ff:ff:00") into a u64. + let address = hex_address.parse_next(input)?; + let combined = ((address.0 as u64) << 40) + | ((address.1 as u64) << 32) + | ((address.2 as u64) << 24) + | ((address.3 as u64) << 16) + | ((address.4 as u64) << 8) + | (address.5 as u64); + Ok(combined) +} + +fn hex_primary(input: &mut &str) -> winnow::PResult { + use winnow::token::take_while; + use winnow::Parser; + + take_while(2, |c: char| c.is_ascii_hexdigit()) + .try_map(|input| u8::from_str_radix(input, 16)) + .parse_next(input) +} + +/// Parse hex address e.g. `de:ad:be:ee:ee:ef` +fn hex_address(input: &mut &str) -> winnow::PResult<(u8, u8, u8, u8, u8, u8)> { + use winnow::seq; + use winnow::Parser as _; + + seq! { + hex_primary, + _: ':', + hex_primary, + _: ':', + hex_primary, + _: ':', + + hex_primary, + _: ':', + hex_primary, + _: ':', + hex_primary, + } + .parse_next(input) +} + +#[cfg(test)] +mod tests { + use super::*; + use winnow::Parser as _; + + #[test] + fn test_id_to_address() { + let id = "Bluetooth#Bluetooth00:00:00:ff:ff:00-de:ad:be:ee:ee:ef"; + let address = id_to_address + .parse(id) + .unwrap_or_else(|err| panic!("{err}")); + assert_eq!(address, 0xdeadbeeeeeef); + } +} diff --git a/crates/bluetooth/src/device/windows/device_info/mod.rs b/crates/bluetooth/src/device/windows/device_info/mod.rs index d55aeac..ea7ec41 100644 --- a/crates/bluetooth/src/device/windows/device_info/mod.rs +++ b/crates/bluetooth/src/device/windows/device_info/mod.rs @@ -108,38 +108,30 @@ impl BluetoothDeviceInfo { /// - is_connected /// - last_used /// - last_updated - pub(crate) fn update_info(&mut self) -> Result<(), BluetoothDeviceInfoError> { + pub(crate) fn update_info( + &mut self, + is_connected: bool, + ) -> Result<(), BluetoothDeviceInfoError> { let device = DeviceInstance::new(self.device_instance); self.battery_level = device.get_device_property(&DEVPKEY_Bluetooth_Battery)?; self.last_updated = LocalTime::now(); - let sys_device = { - let mut devices = match super::device_searcher::get_bluetooth_devices() { - Ok(devices) => devices, - Err(err) => { - tracing::error!("{err}"); - return Ok(()); - } - }; - devices.remove(&self.address) - }; - // NOTE: `is_connected` & `last_used` must be taken by device_search to get a decent value, so the information is merged. - self.is_connected = sys_device - .as_ref() - .map(|device| device.is_connected) - .unwrap_or_default(); - self.last_used = sys_device - .map(|device| device.last_used) - .unwrap_or_default(); + self.is_connected = is_connected; + if is_connected { + self.last_used = LocalTime::now(); + } Ok(()) } } /// Gets the list of Bluetooth devices and their properties. -pub(crate) fn get_bluetooth_devices() -> Result { +/// +/// # Errors +/// Failed to get the list of Bluetooth devices +pub fn get_bluetooth_devices() -> Result { let buffer = { let buffer_size = { let mut buffer_size: u32 = 0; diff --git a/crates/bluetooth/src/device/windows/device_searcher.rs b/crates/bluetooth/src/device/windows/device_searcher.rs index 3567916..2789b11 100644 --- a/crates/bluetooth/src/device/windows/device_searcher.rs +++ b/crates/bluetooth/src/device/windows/device_searcher.rs @@ -1,4 +1,4 @@ -use crate::BluetoothDeviceInfo; +use crate::{categories::category::Category, BluetoothDeviceInfo}; use std::{collections::HashMap, mem, ptr}; use windows::Win32::{ Devices::Bluetooth::{ @@ -70,8 +70,9 @@ pub(crate) fn get_bluetooth_devices() -> windows::core::Result for BluetoothDeviceInfo { fn from(value: SysBluetoothDeviceInfo) -> Self { Self { - is_connected: value.fConnected.as_bool(), address: unsafe { value.Address.Anonymous.ullLong }, + category: Category::try_from(value.ulClassofDevice).unwrap_or_default(), + is_connected: value.fConnected.as_bool(), last_used: crate::device::device_info::LocalTime { year: value.stLastUsed.wYear, month: value.stLastUsed.wMonth, diff --git a/crates/bluetooth/src/device/windows/inspect.rs b/crates/bluetooth/src/device/windows/inspect.rs new file mode 100644 index 0000000..c1ba292 --- /dev/null +++ b/crates/bluetooth/src/device/windows/inspect.rs @@ -0,0 +1,57 @@ +use windows::core::{IInspectable, Interface as _, HSTRING}; +use windows::Foundation::Collections::IKeyValuePair; +use windows::Foundation::IReference; + +use crate::device::device_info::LocalTime; + +/// print property key and value. +/// +/// # Errors +/// If failed to type cast +pub fn reveal_value(prop: IKeyValuePair) -> windows::core::Result<()> { + let key = prop.Key()?; + let value = prop.Value()?; + + match value.GetRuntimeClassName()?.to_string().as_str() { + "Windows.Foundation.IReference`1" => { + let val: bool = value.cast::>()?.Value()?; + println!("{} = {} (Boolean)", key, val); + } + "Windows.Foundation.IReference`1" => { + let val: HSTRING = value.cast::>()?.Value()?; + println!("{} = {} (String)", key, val); + } + "Windows.Foundation.IReference`1" => { + let val: u8 = value.cast::>()?.Value()?; + println!("{} = {} (UInt8)", key, val); + } + "Windows.Foundation.IReference`1" => { + let val = value + .cast::>()? + .Value()?; + + let utc_time = windows_datetime_to_chrono(val.UniversalTime); + println!("{} = {:?} (DateTime)", key, LocalTime::from_utc(&utc_time)); + } + unknown => { + println!("{key} = "); + } + } + + Ok(()) +} + +fn windows_datetime_to_chrono(universal_time: i64) -> chrono::DateTime { + use chrono::TimeZone as _; + // Windows FILETIME epoch (1601-01-01) to Unix epoch (1970-01-01) in 100ns units + const EPOCH_DIFFERENCE_100NS: i64 = 11_644_473_600 * 10_000_000; + // Adjust to Unix epoch + let unix_time_100ns = universal_time - EPOCH_DIFFERENCE_100NS; + // Convert 100ns to seconds and nanoseconds + let seconds = unix_time_100ns / 10_000_000; + let nanoseconds = (unix_time_100ns % 10_000_000) * 100; + // Create chrono::DateTime + chrono::Utc + .timestamp_opt(seconds, nanoseconds as u32) + .unwrap() +} diff --git a/crates/bluetooth/src/device/windows/mod.rs b/crates/bluetooth/src/device/windows/mod.rs index a2d95f5..84853c3 100644 --- a/crates/bluetooth/src/device/windows/mod.rs +++ b/crates/bluetooth/src/device/windows/mod.rs @@ -1,3 +1,5 @@ +mod address_parser; pub mod device_info; -pub mod device_searcher; +mod device_searcher; +pub mod inspect; pub mod watch; diff --git a/crates/bluetooth/src/device/windows/watch.rs b/crates/bluetooth/src/device/windows/watch.rs index b657d00..48438b2 100644 --- a/crates/bluetooth/src/device/windows/watch.rs +++ b/crates/bluetooth/src/device/windows/watch.rs @@ -7,6 +7,7 @@ use windows::Devices::Enumeration::{ use windows::Foundation::Collections::IIterable; use windows::Foundation::TypedEventHandler; +use super::address_parser::id_to_address; use super::device_info::get_bluetooth_devices; use crate::device::device_info::Devices; use crate::BluetoothDeviceInfo; @@ -36,11 +37,17 @@ impl Drop for Watcher { } } -// ref list: https://learn.microsoft.com/ja-jp/windows/win32/properties/devices-bumper +// Watch targets list +// ref: https://learn.microsoft.com/windows/win32/properties/devices-bumper const DEVICE_ADDRESS: &str = "System.Devices.Aep.DeviceAddress"; -const IS_CONNECTED: &str = "System.Devices.Aep.IsConnected"; // https://learn.microsoft.com/windows/win32/properties/props-system-devices-aep-isconnected +/// https://learn.microsoft.com/windows/win32/properties/props-system-devices-aep-isconnected +const IS_CONNECTED: &str = "System.Devices.Aep.IsConnected"; const LAST_CONNECTED_TIME: &str = "System.DeviceInterface.Bluetooth.LastConnectedTime"; +/// TODO: It is unclear whether DEVPKEY watch is possible in PKEY queries. If it is not possible, we may need to switch to interval format. +/// DEVPKEY_Bluetooth_Battery +const BLUETOOTH_BATTERY: &str = "{104ea319-6ee2-4701-bd47-8ddbf425bbe5} 2"; + impl Watcher { /// Creates a new `Watcher`. /// @@ -60,6 +67,7 @@ impl Watcher { // NOTE: For some reason, I can't get it without specifying `kind`.(I've actually tried it myself and confirmed it). // ref: https://zenn.dev/link/comments/5e86a7fdbe07a7 let kind: IIterable = vec![ + HSTRING::from(BLUETOOTH_BATTERY), HSTRING::from(DEVICE_ADDRESS), HSTRING::from(IS_CONNECTED), // IsConnected is important, without it the update event will not be triggered on disconnect and add. HSTRING::from(LAST_CONNECTED_TIME), @@ -88,10 +96,24 @@ impl Watcher { }; match DEVICES.get_mut(&address) { - Some(mut dev) => match dev.value_mut().update_info() { - Ok(_) => update_fn(dev.value()), - Err(err) => tracing::error!("{err}"), - }, + Some(mut dev) => { + let map = device.Properties()?; + + // for prop in &map { + // dbg!(address); + // type_to_value(prop)?; + // } + + // In my tests, the only properties selected by kind that exist are the ones the device was able to get via pnp. + // Therefore, any device that exists in DashMap should be able to retrieve it. + let is_connected = + map.HasKey(&HSTRING::from(IS_CONNECTED)).unwrap_or_default(); + + match dev.value_mut().update_info(is_connected) { + Ok(()) => update_fn(dev.value()), + Err(err) => tracing::error!("{err}"), + } + } None => { tracing::trace!("This Device address is not found in DashMap: {address}"); } @@ -147,90 +169,30 @@ impl Watcher { } } -/// Convert address string (e.g., `Bluetooth#Bluetooth00:00:00:ff:ff:00-de:ad:be:ee:ee:ef`) into a u64. -fn id_to_address(id: &mut &str) -> winnow::PResult { - use winnow::prelude::Parser as _; - - let input = id; - let prefix = "Bluetooth#Bluetooth"; - let _ = (prefix, hex_address, '-').parse_next(input)?; - - // Convert address string (e.g., "00:00:00:ff:ff:00") into a u64. - let address = hex_address.parse_next(input)?; - let combined = ((address.0 as u64) << 40) - | ((address.1 as u64) << 32) - | ((address.2 as u64) << 24) - | ((address.3 as u64) << 16) - | ((address.4 as u64) << 8) - | (address.5 as u64); - Ok(combined) -} - -fn hex_primary(input: &mut &str) -> winnow::PResult { - use winnow::token::take_while; - use winnow::Parser; - - take_while(2, |c: char| c.is_ascii_hexdigit()) - .try_map(|input| u8::from_str_radix(input, 16)) - .parse_next(input) -} - -/// Parse hex address e.g. `de:ad:be:ee:ee:ef` -fn hex_address(input: &mut &str) -> winnow::PResult<(u8, u8, u8, u8, u8, u8)> { - use winnow::seq; - use winnow::Parser as _; - - seq! { - hex_primary, - _: ':', - hex_primary, - _: ':', - hex_primary, - _: ':', - - hex_primary, - _: ':', - hex_primary, - _: ':', - hex_primary, - } - .parse_next(input) -} - -/// Watch bluetooth classic devices. -#[allow(unused)] #[cfg(test)] mod tests { use super::*; use std::sync::Arc; - use winnow::Parser as _; - - #[test] - fn test_id_to_address() { - let id = "Bluetooth#Bluetooth00:00:00:ff:ff:00-de:ad:be:ee:ee:ef"; - let address = id_to_address - .parse(id) - .unwrap_or_else(|err| panic!("{err}")); - assert_eq!(address, 0xdeadbeeeeeef); - } #[ignore = "Can't watch it on CI."] - #[cfg_attr(feature = "tracing", quick_tracing::try_init)] + #[quick_tracing::try_init] #[test] fn watch_test() -> crate::errors::Result<()> { + tracing::debug!("{:#?}", DEVICES.as_ref()); + let watcher = Arc::new(Watcher::new(|_| ())?); watcher.start()?; - dbg!("Started"); + tracing::info!("Started"); - let cloned = Arc::clone(&watcher); - let stop_handle = std::thread::spawn(move || -> windows::core::Result<()> { - for i in 0..15 { - dbg!(i); - std::thread::sleep(std::time::Duration::from_secs(1)); - } - cloned.stop() - }); - stop_handle.join().unwrap()?; + std::thread::sleep(std::time::Duration::from_secs(15)); + watcher.stop()?; + + // let cloned = Arc::clone(&watcher); + // let stop_handle = std::thread::spawn(move || -> windows::core::Result<()> { + // std::thread::sleep(std::time::Duration::from_secs(15)); + // cloned.stop() + // }); + // stop_handle.join().unwrap()?; Ok(()) } diff --git a/cspell.jsonc b/cspell.jsonc index 8b75152..570b367 100644 --- a/cspell.jsonc +++ b/cspell.jsonc @@ -18,6 +18,7 @@ "LOCALMFG", "PCSTR", "PCWSTR", + "PKEY", "repr", "serde", "tauri", diff --git a/gui/backend/src/cmd/device_watcher.rs b/gui/backend/src/cmd/device_watcher.rs index bd34ce2..c2d403c 100644 --- a/gui/backend/src/cmd/device_watcher.rs +++ b/gui/backend/src/cmd/device_watcher.rs @@ -1,7 +1,6 @@ -use super::config::read_config; -use super::config::write_config; use super::supports::notify::notify; use super::system_tray::{default_tray_inner, update_tray_inner}; +use crate::cmd::config::{read_config_sync, write_config_sync}; use crate::err_log; use crate::err_log_to_string; use crate::error::Error; @@ -47,8 +46,8 @@ pub async fn restart_device_watcher_inner(app: &AppHandle) -> crate::error::Resu }; // Update tray icon - let config = read_config(app.clone()).unwrap_or_else(|_| { - err_log!(write_config(app.clone(), Default::default())); + let config = read_config_sync(app.clone()).unwrap_or_else(|_| { + err_log!(write_config_sync(app.clone(), Default::default())); Default::default() }); if let Some(info) = devices.get(&config.address) { @@ -91,8 +90,8 @@ fn update_devices(app: &AppHandle, info: &BluetoothDeviceInfo) { tracing::error!("{err}"); } - let config = read_config(app.clone()).unwrap_or_else(|_| { - err_log!(write_config(app.clone(), Default::default())); + let config = read_config_sync(app.clone()).unwrap_or_else(|_| { + err_log!(write_config_sync(app.clone(), Default::default())); Default::default() }); From 5fe695aad56adf643dac04e692998151f38b0ad8 Mon Sep 17 00:00:00 2001 From: SARDONYX-sard <68905624+SARDONYX-sard@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:07:08 +0900 Subject: [PATCH 2/6] feat: add interval command --- gui/backend/src/cmd/config.rs | 32 ++++++-- gui/backend/src/cmd/interval.rs | 75 +++++++++++++++++++ gui/backend/src/cmd/mod.rs | 20 +++-- .../src/services/api/bluetooth_finder.ts | 8 ++ 4 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 gui/backend/src/cmd/interval.rs diff --git a/gui/backend/src/cmd/config.rs b/gui/backend/src/cmd/config.rs index 7444f0e..d1c7549 100644 --- a/gui/backend/src/cmd/config.rs +++ b/gui/backend/src/cmd/config.rs @@ -1,6 +1,5 @@ -use std::{fs::read_to_string, path::PathBuf}; - use crate::err_log_to_string; +use std::{fs::read_to_string, path::PathBuf}; use tauri::{AppHandle, Manager}; #[derive(Debug, serde::Deserialize, serde::Serialize)] @@ -35,13 +34,36 @@ fn get_config_path(app: &AppHandle) -> Result { // NOTE: tauri::command couldn't use `&Config` #[tauri::command] -pub fn write_config(app: AppHandle, config: Config) -> Result<(), String> { +pub async fn write_config(app: AppHandle, config: Config) -> Result<(), String> { let config = err_log_to_string!(serde_json::to_string(&config))?; - err_log_to_string!(std::fs::write(get_config_path(&app)?, config)) + err_log_to_string!(tokio::fs::write(get_config_path(&app)?, config).await) } #[tauri::command] -pub fn read_config(app: AppHandle) -> Result { +pub async fn read_config(app: AppHandle) -> Result { + Ok(match read_to_string(get_config_path(&app)?) { + Ok(content) => match serde_json::from_str(&content) { + Ok(config) => config, + Err(err) => { + let err = format!("Failed to parse config: {err}"); + tracing::error!("{err}"); + Config::default() + } + }, + Err(err) => { + let err = format!("Failed to read config: {err}"); + tracing::error!("{err}"); + Config::default() + } + }) +} + +pub fn write_config_sync(app: AppHandle, config: Config) -> Result<(), String> { + let config = err_log_to_string!(serde_json::to_string(&config))?; + err_log_to_string!(std::fs::write(get_config_path(&app)?, config)) +} + +pub fn read_config_sync(app: AppHandle) -> Result { Ok(match read_to_string(get_config_path(&app)?) { Ok(content) => match serde_json::from_str(&content) { Ok(config) => config, diff --git a/gui/backend/src/cmd/interval.rs b/gui/backend/src/cmd/interval.rs new file mode 100644 index 0000000..dafbd76 --- /dev/null +++ b/gui/backend/src/cmd/interval.rs @@ -0,0 +1,75 @@ +use super::config::{read_config, write_config_sync}; +use crate::cmd::supports::notify; +use crate::cmd::system_tray::update_tray_inner; +use crate::err_log; +use bluetooth::device::windows::{device_info::get_bluetooth_devices, watch::DEVICES}; +use std::{ + sync::atomic::{AtomicU64, Ordering}, + time::Duration, +}; +use tauri::{AppHandle, Emitter as _, Manager as _}; +use timer::{clear_interval, set_interval}; + +static INTERVAL_ID: AtomicU64 = AtomicU64::new(0); + +/// # NOTE +/// The callback fn cannot return a Result, so write only error log. +#[tauri::command] +pub async fn restart_interval(app: AppHandle) { + tracing::trace!("`restart_interval` was called."); + let id = INTERVAL_ID.load(Ordering::Acquire); + if id != 0 { + clear_interval(id).await; + }; + + let config = read_config(app.clone()).await.unwrap_or_else(|_| { + err_log!(write_config_sync(app.clone(), Default::default())); + Default::default() + }); + let duration = Duration::from_secs(config.battery_query_duration_minutes * 60); // minutes -> seconds + + let id = set_interval( + move || { + // Callbacks in the interval may survive until program termination in the worst case. + // Therefore, they become 'static' and must be cloned. + let app = app.clone(); + let address = config.address; + let window = app.get_webview_window("main").unwrap(); + + async move { + // NOTE: The callback fn cannot return a Result, so write only error log. + let devices = match get_bluetooth_devices() { + Ok(devices) => devices, + Err(err) => { + tracing::error!("{err}"); + return; + } + }; + + if let Some(dev) = devices.get(&address) { + let battery_level = dev.battery_level as u64; + if battery_level <= config.notify_battery_level { + let notify_msg = format!("Battery power is low: {battery_level}%"); + err_log!(notify::notify(&app, ¬ify_msg)); + } + + let dev_name = &dev.friendly_name; + err_log!(update_tray_inner(dev_name, battery_level, dev.is_connected)); + }; + + err_log!(window.emit("bt_monitor://update_devices", &devices)); + + // Replace all + for (address, device) in devices { + if let Some(mut value) = DEVICES.get_mut(&address) { + *value.value_mut() = device; + } + } + } + }, + duration, + ) + .await; + + INTERVAL_ID.store(id, Ordering::Release); +} diff --git a/gui/backend/src/cmd/mod.rs b/gui/backend/src/cmd/mod.rs index c3f5648..2cf9a2b 100644 --- a/gui/backend/src/cmd/mod.rs +++ b/gui/backend/src/cmd/mod.rs @@ -1,14 +1,17 @@ mod config; -pub(crate) mod device_watcher; -pub mod supports; -pub(super) mod system_tray; +mod interval; +mod supports; +mod system_tray; + +pub(super) mod device_watcher; use crate::err_log_to_string; use tauri::{Builder, Wry}; +use tokio::fs; #[tauri::command] pub(crate) async fn change_log_level(log_level: Option<&str>) -> Result<(), String> { - tracing::debug!("Selected log level: {:?}", log_level); + tracing::trace!("Selected log level: {:?}", log_level); err_log_to_string!(crate::log::change_level(log_level.unwrap_or("error"))) } @@ -17,7 +20,7 @@ pub(crate) async fn change_log_level(log_level: Option<&str>) -> Result<(), Stri /// (there was a case that the order of some data in contents was switched). #[tauri::command] pub(crate) async fn write_file(path: &str, content: &str) -> Result<(), String> { - err_log_to_string!(std::fs::write(path, content)) + err_log_to_string!(fs::write(path, content).await) } pub(crate) trait CommandsRegister { @@ -29,13 +32,14 @@ impl CommandsRegister for Builder { fn impl_commands(self) -> Self { self.invoke_handler(tauri::generate_handler![ change_log_level, - device_watcher::restart_device_watcher, + config::read_config, + config::write_config, device_watcher::get_devices, + device_watcher::restart_device_watcher, + interval::restart_interval, system_tray::default_tray, system_tray::update_tray, write_file, - config::read_config, - config::write_config, ]) } } diff --git a/gui/frontend/src/services/api/bluetooth_finder.ts b/gui/frontend/src/services/api/bluetooth_finder.ts index 122c767..8f95f3a 100644 --- a/gui/frontend/src/services/api/bluetooth_finder.ts +++ b/gui/frontend/src/services/api/bluetooth_finder.ts @@ -71,3 +71,11 @@ export async function restartDeviceWatcher() { export async function getDevices() { return await invoke('get_devices'); } + +/** + * Restart interval to get bluetooth device information. + * @throws `Error` + */ +export async function restartInterval() { + await invoke('restart_interval'); +} From cdcedb0fbef5e7d818c9eea3a354254ad63b5d58 Mon Sep 17 00:00:00 2001 From: SARDONYX-sard <68905624+SARDONYX-sard@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:10:08 +0900 Subject: [PATCH 3/6] docs: add comments --- gui/backend/src/cmd/interval.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gui/backend/src/cmd/interval.rs b/gui/backend/src/cmd/interval.rs index dafbd76..c3d991b 100644 --- a/gui/backend/src/cmd/interval.rs +++ b/gui/backend/src/cmd/interval.rs @@ -1,3 +1,6 @@ +/// # Why need this code? +/// DeviceWatcher triggers when a device is connected or disconnected, but cannot WATCH DEVPKEY for battery information. +/// ThatFor this reason, it is currently not possible to use the ITherefore, it is currently using interval processing. use super::config::{read_config, write_config_sync}; use crate::cmd::supports::notify; use crate::cmd::system_tray::update_tray_inner; @@ -10,6 +13,7 @@ use std::{ use tauri::{AppHandle, Emitter as _, Manager as _}; use timer::{clear_interval, set_interval}; +/// The task is singleton. static INTERVAL_ID: AtomicU64 = AtomicU64::new(0); /// # NOTE From 7abea9daea2a6d0817f25e0d6f3d6e67c25ef1a8 Mon Sep 17 00:00:00 2001 From: SARDONYX-sard <68905624+SARDONYX-sard@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:23:20 +0900 Subject: [PATCH 4/6] revert(frontend): revert interval config field --- .../organisms/BluetoothGrid/ConfigFields.tsx | 37 +++++++++++++++++++ locales/en-US.json | 20 +++++----- locales/ja-JP.json | 20 +++++----- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/gui/frontend/src/components/organisms/BluetoothGrid/ConfigFields.tsx b/gui/frontend/src/components/organisms/BluetoothGrid/ConfigFields.tsx index 0db5862..0efbbf1 100644 --- a/gui/frontend/src/components/organisms/BluetoothGrid/ConfigFields.tsx +++ b/gui/frontend/src/components/organisms/BluetoothGrid/ConfigFields.tsx @@ -11,6 +11,7 @@ export const ConfigFields = () => { const { t } = useTranslation(); const [isAutoStart, setIsAutoStart] = useState(null); const [conf, setConf] = useState(null); + const interval = conf?.battery_query_duration_minutes; const warnTime = conf?.notify_battery_level; useEffect(() => { @@ -45,6 +46,23 @@ export const ConfigFields = () => { } }, [isAutoStart]); + const handleInterval: ChangeEventHandler = (e) => { + const newValue = Number(e.target.value); + const newTime = Number.isNaN(newValue) ? 30 : newValue; + + if (conf) { + if (conf.battery_query_duration_minutes === newTime) { + return; + } + + setConf({ + ...conf, + // biome-ignore lint/style/useNamingConvention: + battery_query_duration_minutes: newTime, + }); + } + }; + const handleWarnPerLevel: ChangeEventHandler = (e) => { const newValue = Number(e.target.value); const newPer = Number.isNaN(newValue) ? 20 : newValue; @@ -75,6 +93,25 @@ export const ConfigFields = () => { )} + {interval !== undefined ? ( + + + + ) : ( + + )} + {warnTime !== undefined ? ( Date: Tue, 21 Jan 2025 22:24:02 +0900 Subject: [PATCH 5/6] chore(locale): sort key by alpha --- locales/ko-KR.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/locales/ko-KR.json b/locales/ko-KR.json index f8a6d7c..300ad0f 100644 --- a/locales/ko-KR.json +++ b/locales/ko-KR.json @@ -31,6 +31,8 @@ "lang-preset-auto": "자동", "lang-preset-custom": "사용자 지정", "lang-preset-label": "언어", + "last-updated": "최근 업데이트", + "last-used": "최근 사용", "log-level-list-label": "로그 수준", "log-level-list-tooltip": "마이너 로그 수준은 더 중요한 로그 수준을 포함합니다. (예: 오류 ⊂ 정보)", "log-level-list-tooltip2": "디버그: 변환된 조건의 진행 과정에 대한 데이터를 기록합니다.", @@ -48,6 +50,12 @@ "open-log-dir-btn": "로그 (디렉터리)", "open-log-dir-tooltip": "로그 저장 위치를 엽니다.", "open-log-tooltip": "현재 로그 파일을 엽니다. (응용 프로그램이 실행될 때마다 새 로그 파일로 회전합니다.)", + "relativeTime": { + "days": "일 전", + "hours": "시간 전", + "minutes": "분 전", + "seconds": "초 전" + }, "restart-btn": "다시 시작", "restart-tooltip": "설정을 적용하고 블루투스 정보를 다시 획득합니다.", "restarting-btn": "다시 시작...", @@ -63,13 +71,5 @@ "target-bt-id": "트레이에 표시", "target-bt-id-tooltip": "작업 표시줄의 트레이에 이 장치의 배터리 정보를 표시합니다.", "warn-limit-battery": "배터리 경고(%)", - "warn-limit-battery-tooltip": "배터리 잔량이 몇 퍼센트 남았는지 알려줍니다.", - "relativeTime": { - "seconds": "초 전", - "minutes": "분 전", - "hours": "시간 전", - "days": "일 전" - }, - "last-updated": "최근 업데이트", - "last-used": "최근 사용" + "warn-limit-battery-tooltip": "배터리 잔량이 몇 퍼센트 남았는지 알려줍니다." } From 362e8983056716061cfdb01bd05e3c7d012499ca Mon Sep 17 00:00:00 2001 From: SARDONYX-sard <68905624+SARDONYX-sard@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:40:19 +0900 Subject: [PATCH 6/6] feat: insert interval on setup --- gui/backend/src/cmd/interval.rs | 6 +++--- gui/backend/src/cmd/mod.rs | 2 +- gui/backend/src/cmd/system_tray.rs | 3 +-- gui/backend/src/setup/mod.rs | 6 +++++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/gui/backend/src/cmd/interval.rs b/gui/backend/src/cmd/interval.rs index c3d991b..ca0bce5 100644 --- a/gui/backend/src/cmd/interval.rs +++ b/gui/backend/src/cmd/interval.rs @@ -20,7 +20,7 @@ static INTERVAL_ID: AtomicU64 = AtomicU64::new(0); /// The callback fn cannot return a Result, so write only error log. #[tauri::command] pub async fn restart_interval(app: AppHandle) { - tracing::trace!("`restart_interval` was called."); + tracing::debug!("`restart_interval` was called."); let id = INTERVAL_ID.load(Ordering::Acquire); if id != 0 { clear_interval(id).await; @@ -34,8 +34,8 @@ pub async fn restart_interval(app: AppHandle) { let id = set_interval( move || { - // Callbacks in the interval may survive until program termination in the worst case. - // Therefore, they become 'static' and must be cloned. + tracing::debug!("`restart_interval` closure was called."); + let app = app.clone(); let address = config.address; let window = app.get_webview_window("main").unwrap(); diff --git a/gui/backend/src/cmd/mod.rs b/gui/backend/src/cmd/mod.rs index 2cf9a2b..5d9cc79 100644 --- a/gui/backend/src/cmd/mod.rs +++ b/gui/backend/src/cmd/mod.rs @@ -1,9 +1,9 @@ mod config; -mod interval; mod supports; mod system_tray; pub(super) mod device_watcher; +pub(super) mod interval; use crate::err_log_to_string; use tauri::{Builder, Wry}; diff --git a/gui/backend/src/cmd/system_tray.rs b/gui/backend/src/cmd/system_tray.rs index b98078c..fc1dbc5 100644 --- a/gui/backend/src/cmd/system_tray.rs +++ b/gui/backend/src/cmd/system_tray.rs @@ -5,13 +5,12 @@ use tauri::image::Image; /// /// # Panics /// 0 <= battery_level <= 100 +#[tracing::instrument(level = "trace")] pub fn update_tray_inner( device_name: &str, battery_level: u64, is_connected: bool, ) -> tauri::Result<()> { - tracing::debug!("Change to {battery_level} battery icon"); - let battery_icon = if is_connected { match battery_level { 0 => include_bytes!("../../icons/battery/battery-0.png"), diff --git a/gui/backend/src/setup/mod.rs b/gui/backend/src/setup/mod.rs index 51a92db..e7a95ab 100644 --- a/gui/backend/src/setup/mod.rs +++ b/gui/backend/src/setup/mod.rs @@ -3,7 +3,10 @@ mod window_event; use self::tray_menu::new_tray_menu; use self::window_event::window_event; -use crate::{cmd::device_watcher::restart_device_watcher_inner, err_log}; +use crate::{ + cmd::{device_watcher::restart_device_watcher_inner, interval::restart_interval}, + err_log, +}; use tauri::{Builder, Manager, Wry}; pub use tray_menu::TRAY_ICON; @@ -23,6 +26,7 @@ impl SetupsRegister for Builder { tauri::async_runtime::spawn(async move { let app = app; err_log!(restart_device_watcher_inner(&app).await); + restart_interval(app).await; }); Ok(())