Skip to content

Commit

Permalink
Merge pull request #23 from SARDONYX-sard/feature/revert-interval-pat…
Browse files Browse the repository at this point in the history
…tern

Feature/revert interval pattern
  • Loading branch information
SARDONYX-sard authored Jan 21, 2025
2 parents ff6cf1d + 362e898 commit 359262f
Show file tree
Hide file tree
Showing 19 changed files with 387 additions and 152 deletions.
2 changes: 1 addition & 1 deletion crates/bluetooth/src/device/device_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl LocalTime {
}
}

pub fn from_utc(utc_time: chrono::DateTime<chrono::Utc>) -> Self {
pub fn from_utc(utc_time: &chrono::DateTime<chrono::Utc>) -> Self {
let time = utc_time.with_timezone(&chrono::Local);
Self {
year: time.year() as u16,
Expand Down
64 changes: 64 additions & 0 deletions crates/bluetooth/src/device/windows/address_parser.rs
Original file line number Diff line number Diff line change
@@ -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<u64> {
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<u8> {
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);
}
}
32 changes: 12 additions & 20 deletions crates/bluetooth/src/device/windows/device_info/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Devices, BluetoothDeviceInfoError> {
///
/// # Errors
/// Failed to get the list of Bluetooth devices
pub fn get_bluetooth_devices() -> Result<Devices, BluetoothDeviceInfoError> {
let buffer = {
let buffer_size = {
let mut buffer_size: u32 = 0;
Expand Down
5 changes: 3 additions & 2 deletions crates/bluetooth/src/device/windows/device_searcher.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::BluetoothDeviceInfo;
use crate::{categories::category::Category, BluetoothDeviceInfo};
use std::{collections::HashMap, mem, ptr};
use windows::Win32::{
Devices::Bluetooth::{
Expand Down Expand Up @@ -70,8 +70,9 @@ pub(crate) fn get_bluetooth_devices() -> windows::core::Result<HashMap<u64, Blue
impl From<SysBluetoothDeviceInfo> 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,
Expand Down
57 changes: 57 additions & 0 deletions crates/bluetooth/src/device/windows/inspect.rs
Original file line number Diff line number Diff line change
@@ -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<HSTRING, IInspectable>) -> windows::core::Result<()> {
let key = prop.Key()?;
let value = prop.Value()?;

match value.GetRuntimeClassName()?.to_string().as_str() {
"Windows.Foundation.IReference`1<Boolean>" => {
let val: bool = value.cast::<IReference<bool>>()?.Value()?;
println!("{} = {} (Boolean)", key, val);
}
"Windows.Foundation.IReference`1<String>" => {
let val: HSTRING = value.cast::<IReference<HSTRING>>()?.Value()?;
println!("{} = {} (String)", key, val);
}
"Windows.Foundation.IReference`1<UInt8>" => {
let val: u8 = value.cast::<IReference<u8>>()?.Value()?;
println!("{} = {} (UInt8)", key, val);
}
"Windows.Foundation.IReference`1<Windows.Foundation.DateTime>" => {
let val = value
.cast::<IReference<windows::Foundation::DateTime>>()?
.Value()?;

let utc_time = windows_datetime_to_chrono(val.UniversalTime);
println!("{} = {:?} (DateTime)", key, LocalTime::from_utc(&utc_time));
}
unknown => {
println!("{key} = <Unknown Type: {unknown}>");
}
}

Ok(())
}

fn windows_datetime_to_chrono(universal_time: i64) -> chrono::DateTime<chrono::Utc> {
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()
}
4 changes: 3 additions & 1 deletion crates/bluetooth/src/device/windows/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod address_parser;
pub mod device_info;
pub mod device_searcher;
mod device_searcher;
pub mod inspect;
pub mod watch;
120 changes: 41 additions & 79 deletions crates/bluetooth/src/device/windows/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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`.
///
Expand All @@ -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<HSTRING> = 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),
Expand Down Expand Up @@ -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}");
}
Expand Down Expand Up @@ -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<u64> {
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<u8> {
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(())
}
Expand Down
1 change: 1 addition & 0 deletions cspell.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"LOCALMFG",
"PCSTR",
"PCWSTR",
"PKEY",
"repr",
"serde",
"tauri",
Expand Down
Loading

0 comments on commit 359262f

Please sign in to comment.