mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-15 07:06:11 -04:00
[fp-rs] Implemented classic device pairing
This commit is contained in:
@@ -29,6 +29,7 @@ thiserror = "1.0.43"
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.48", features = [
|
||||
"Devices_Bluetooth",
|
||||
"Devices_Enumeration",
|
||||
"Devices_Bluetooth_Advertisement",
|
||||
"Foundation",
|
||||
] }
|
||||
|
||||
@@ -93,7 +93,7 @@ impl From<BleAddress> for u64 {
|
||||
impl From<ClassicAddress> for u64 {
|
||||
fn from(addr: ClassicAddress) -> Self {
|
||||
let mut bytes = [0u8; 8];
|
||||
bytes.copy_from_slice(&addr.0);
|
||||
bytes[..6].copy_from_slice(&addr.0);
|
||||
|
||||
u64::from_le_bytes(bytes)
|
||||
}
|
||||
|
||||
@@ -12,12 +12,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::{Address, BluetoothError, PairingResult};
|
||||
|
||||
/// Concrete types implementing this trait represent Bluetooth Peripheral devices.
|
||||
/// They provide methods for retrieving device info and running device actions,
|
||||
/// such as pairing.
|
||||
use super::BluetoothError;
|
||||
|
||||
pub trait Device {
|
||||
#[async_trait]
|
||||
pub trait Device: Sized {
|
||||
/// Retrieve the name advertised by this device.
|
||||
fn name(&self) -> Result<String, BluetoothError>;
|
||||
|
||||
/// Retrieve this device's Bluetooth address information.
|
||||
fn address(&self) -> Address;
|
||||
|
||||
/// Attempt pairing with the peripheral device.
|
||||
async fn pair(&self) -> Result<PairingResult, BluetoothError>;
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ use thiserror::Error;
|
||||
#[non_exhaustive]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BluetoothError {
|
||||
/// Reported when Bluetooth device pairing fails.
|
||||
#[error("pairing error: {0}")]
|
||||
PairingFailed(String),
|
||||
/// Indicates that the operation was rejected because the system is not in
|
||||
/// a state required for the operation's execution.
|
||||
/// E.g. The user calls `stop_scan()` or polls the advertisement stream
|
||||
/// before calling `start_scan()`.
|
||||
#[error("failed precondition: {0}")]
|
||||
FailedPrecondition(String),
|
||||
/// Reported when the user calls an operation that is supported by their
|
||||
@@ -40,3 +42,15 @@ pub enum BluetoothError {
|
||||
#[error("internal error: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
/// Abstraction around platform-specific pairing status enums.
|
||||
/// `PairingResult::Failure` should eventually be converted to
|
||||
/// `BluetoothError::PairingFailed`.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum PairingResult {
|
||||
Success,
|
||||
AlreadyPaired,
|
||||
AlreadyInProgress,
|
||||
Failure(String),
|
||||
}
|
||||
|
||||
@@ -18,18 +18,24 @@
|
||||
|
||||
pub mod common;
|
||||
|
||||
pub use common::{Adapter, BluetoothError, Device};
|
||||
pub use common::{Adapter, Address, BluetoothError, ClassicAddress, Device};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(windows)] {
|
||||
mod windows;
|
||||
use self::windows::BleAdapter;
|
||||
use self::windows::{ClassicDevice, BleAdapter};
|
||||
} else {
|
||||
mod unsupported;
|
||||
use unsupported::BleAdapter;
|
||||
use unsupported::{ClassicDevice, BleAdapter};
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn default_adapter() -> Result<impl Adapter, BluetoothError> {
|
||||
BleAdapter::default().await
|
||||
}
|
||||
|
||||
pub async fn new_classic_device(
|
||||
addr: ClassicAddress,
|
||||
) -> Result<impl Device, BluetoothError> {
|
||||
ClassicDevice::new(addr).await
|
||||
}
|
||||
|
||||
@@ -13,42 +13,162 @@
|
||||
// limitations under the License.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use windows::Devices::Bluetooth::{
|
||||
// Enum describing the type of address (public, random, unspecified).
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothaddresstype?view=winrt-22621
|
||||
BluetoothAddressType,
|
||||
|
||||
// Struct for interacting with and pairing to a discovered BLE device.
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice?view=winrt-22621
|
||||
BluetoothLEDevice,
|
||||
use tracing::{info, warn};
|
||||
use windows::{
|
||||
Devices::{
|
||||
Bluetooth::{
|
||||
// Tuple struct describing the type of address (public, random, unspecified).
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothaddresstype?view=winrt-22621
|
||||
BluetoothAddressType,
|
||||
|
||||
// Struct for interacting with a discovered BT Classic device.
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothdevice?view=winrt-22621
|
||||
BluetoothDevice,
|
||||
|
||||
// Struct for interacting with a discovered BLE device.
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice?view=winrt-22621
|
||||
BluetoothLEDevice,
|
||||
},
|
||||
Enumeration::{
|
||||
// Struct for custom pairing with a device.
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.enumeration.deviceinformationcustompairing?view=winrt-22621
|
||||
DeviceInformationCustomPairing,
|
||||
|
||||
// Tuple struct to indicate the kinds of pairing supported by the application.
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.enumeration.devicepairingkinds?view=winrt-22621
|
||||
DevicePairingKinds,
|
||||
|
||||
// Struct for retrieving data about a PairingRequested event.
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.enumeration.devicepairingrequestedeventargs?view=winrt-22621
|
||||
DevicePairingRequestedEventArgs,
|
||||
},
|
||||
},
|
||||
// Wraps a closure for handling events associated with a struct
|
||||
// (e.g. PairingRequested event in `DeviceInformationCustomPairing`).
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.foundation.typedeventhandler-2?view=winrt-22621
|
||||
Foundation::TypedEventHandler,
|
||||
};
|
||||
|
||||
use crate::bluetooth::common::{BleAddress, BluetoothError, Device};
|
||||
use crate::bluetooth::common::{Address, BleAddress, ClassicAddress, Device, BluetoothError, PairingResult};
|
||||
|
||||
/// Concrete type implementing `Device`, used for Windows BLE.
|
||||
pub struct BleDevice {
|
||||
inner: BluetoothLEDevice,
|
||||
addr: BleAddress,
|
||||
}
|
||||
|
||||
/// Concrete type implementing `Device`, used for Windows Bluetooth Classic.
|
||||
pub struct ClassicDevice {
|
||||
inner: BluetoothDevice,
|
||||
addr: ClassicAddress,
|
||||
}
|
||||
|
||||
impl BleDevice {
|
||||
/// `BleDevice` constructor.
|
||||
pub async fn new(addr: BleAddress) -> Result<Self, BluetoothError> {
|
||||
let kind = BluetoothAddressType::from(addr.get_kind());
|
||||
let addr = u64::from(addr);
|
||||
let raw_addr = u64::from(addr);
|
||||
|
||||
let inner = BluetoothLEDevice::FromBluetoothAddressWithBluetoothAddressTypeAsync(
|
||||
addr, kind,
|
||||
raw_addr, kind,
|
||||
)?
|
||||
.await?;
|
||||
|
||||
Ok(BleDevice { inner })
|
||||
Ok(BleDevice { inner, addr })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Device for BleDevice {
|
||||
fn name(&self) -> Result<String, BluetoothError> {
|
||||
Ok(self.inner.Name()?.to_string())
|
||||
}
|
||||
|
||||
fn address(&self) -> Address {
|
||||
Address::Ble(self.addr)
|
||||
}
|
||||
|
||||
async fn pair(&self) -> Result<PairingResult, BluetoothError> {
|
||||
// BLE Audio isn't supported on Windows natively, so devices can pair
|
||||
// but don't playback. Might possibly work with UWP. Since the Classic
|
||||
// and BLE APIs are very similar, it might be possible to copy-paste
|
||||
// `ClassicDevice::pair` directly.
|
||||
unimplemented!("BLE Pairing is currently unsupported.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ClassicDevice {
|
||||
/// `ClassicDevice` constructor.
|
||||
pub async fn new(addr: ClassicAddress) -> Result<Self, BluetoothError> {
|
||||
let raw_addr = u64::from(addr);
|
||||
|
||||
let inner = BluetoothDevice::FromBluetoothAddressAsync(
|
||||
raw_addr,
|
||||
)?
|
||||
.await?;
|
||||
|
||||
Ok(ClassicDevice { inner, addr })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Device for ClassicDevice {
|
||||
fn name(&self) -> Result<String, BluetoothError> {
|
||||
Ok(self.inner.Name()?.to_string_lossy())
|
||||
}
|
||||
|
||||
fn address(&self) -> Address {
|
||||
Address::Classic(self.addr)
|
||||
}
|
||||
|
||||
async fn pair(&self) -> Result<PairingResult, BluetoothError> {
|
||||
let pair_info = self.inner.DeviceInformation()?.Pairing()?;
|
||||
if pair_info.IsPaired()? {
|
||||
info!("Device already paired");
|
||||
Ok(PairingResult::AlreadyPaired)
|
||||
} else if !pair_info.CanPair()? {
|
||||
info!("Device can't pair");
|
||||
Err(BluetoothError::PairingFailed(String::from("device can't pair")))
|
||||
} else {
|
||||
let custom = pair_info.Custom()?;
|
||||
custom.PairingRequested(&TypedEventHandler::new(
|
||||
|_custom: &Option<DeviceInformationCustomPairing>,
|
||||
event_args: &Option<DevicePairingRequestedEventArgs>,
|
||||
| {
|
||||
if let Some(event_args) = event_args {
|
||||
match event_args.PairingKind()? {
|
||||
DevicePairingKinds::ConfirmOnly => {
|
||||
event_args.Accept()
|
||||
}
|
||||
_ => {
|
||||
warn!("Unsupported pairing kind {:?}", event_args.PairingKind());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Empty pairing event arguments");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
},
|
||||
))?;
|
||||
let res = custom
|
||||
.PairAsync(
|
||||
DevicePairingKinds::ConfirmOnly
|
||||
| DevicePairingKinds::ProvidePin
|
||||
| DevicePairingKinds::ConfirmPinMatch
|
||||
| DevicePairingKinds::DisplayPin,
|
||||
)?
|
||||
.await?;
|
||||
let status = PairingResult::from(res.Status()?);
|
||||
|
||||
match status {
|
||||
PairingResult::Failure(msg) => Err(BluetoothError::PairingFailed(msg)),
|
||||
_ => Ok(status),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod tests {
|
||||
|
||||
@@ -12,10 +12,46 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::bluetooth::common::BluetoothError;
|
||||
use windows::Devices::Enumeration::DevicePairingResultStatus;
|
||||
|
||||
use crate::bluetooth::common::{BluetoothError, PairingResult};
|
||||
|
||||
impl From<windows::core::Error> for BluetoothError {
|
||||
fn from(err: windows::core::Error) -> Self {
|
||||
BluetoothError::System(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.enumeration.devicepairingresultstatus?view=winrt-22621
|
||||
impl From<DevicePairingResultStatus> for PairingResult {
|
||||
fn from(status: DevicePairingResultStatus) -> Self {
|
||||
match status {
|
||||
DevicePairingResultStatus::Paired => PairingResult::Success,
|
||||
DevicePairingResultStatus::AlreadyPaired => {
|
||||
PairingResult::AlreadyPaired
|
||||
}
|
||||
DevicePairingResultStatus::OperationAlreadyInProgress => {
|
||||
PairingResult::AlreadyInProgress
|
||||
}
|
||||
DevicePairingResultStatus::NotReadyToPair => PairingResult::Failure(
|
||||
String::from("the device object is not in a state where it can be paired"),
|
||||
),
|
||||
DevicePairingResultStatus::NotPaired => PairingResult::Failure(String::from("the device object is not currently paired.")),
|
||||
DevicePairingResultStatus::ConnectionRejected => PairingResult::Failure(String::from("the device object rejected the connection.")),
|
||||
DevicePairingResultStatus::TooManyConnections => PairingResult::Failure(String::from("the device object indicated it cannot accept any more incoming connections.")),
|
||||
DevicePairingResultStatus::HardwareFailure => PairingResult::Failure(String::from("the device object indicated there was a hardware failure.")),
|
||||
DevicePairingResultStatus::AuthenticationTimeout => PairingResult::Failure(String::from("the authentication process timed out before it could complete.")),
|
||||
DevicePairingResultStatus::AuthenticationNotAllowed => PairingResult::Failure(String::from("the authentication protocol is not supported, so the device is not paired.")),
|
||||
DevicePairingResultStatus::AuthenticationFailure => PairingResult::Failure(String::from("authentication failed, so the device is not paired. Either the device object or the application rejected the authentication.")),
|
||||
DevicePairingResultStatus::NoSupportedProfiles => PairingResult::Failure(String::from("there are no network profiles for this device object to use.")),
|
||||
DevicePairingResultStatus::ProtectionLevelCouldNotBeMet => PairingResult::Failure(String::from("the minimum level of protection is not supported by the device object or the application.")),
|
||||
DevicePairingResultStatus::AccessDenied => PairingResult::Failure(String::from("your application does not have the appropriate permissions level to pair the device object.")),
|
||||
DevicePairingResultStatus::InvalidCeremonyData => PairingResult::Failure(String::from("the ceremony data was incorrect.")),
|
||||
DevicePairingResultStatus::PairingCanceled => PairingResult::Failure(String::from("the pairing action was canceled before completion.")),
|
||||
DevicePairingResultStatus::RequiredHandlerNotRegistered => PairingResult::Failure(String::from("either the event handler wasn't registered or a required DevicePairingKinds was not supported.",)),
|
||||
DevicePairingResultStatus::RejectedByHandler => PairingResult::Failure(String::from("the application handler rejected the pairing.")),
|
||||
DevicePairingResultStatus::RemoteDeviceHasAssociation => PairingResult::Failure(String::from("the remote device already has an association.")),
|
||||
DevicePairingResultStatus::Failed | _ => PairingResult::Failure(String::from("an unknown failure occurred.")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,18 +18,48 @@ use futures::executor;
|
||||
|
||||
mod bluetooth;
|
||||
|
||||
use bluetooth::{Adapter, Device};
|
||||
use bluetooth::{Adapter, Address, ClassicAddress, Device};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let run = async {
|
||||
let mut adapter = bluetooth::default_adapter().await?;
|
||||
adapter.start_scan_devices()?;
|
||||
|
||||
while let Ok(device) = adapter.next_device().await {
|
||||
println!("found {}", device.name()?)
|
||||
}
|
||||
while let Ok(ble_device) = adapter.next_device().await {
|
||||
let name = ble_device.name()?;
|
||||
|
||||
unreachable!("Done scanning");
|
||||
if name.contains("LE_WF-1000XM3") {
|
||||
println!("FOUND {} ", name);
|
||||
|
||||
let addr: Address = ble_device.address();
|
||||
|
||||
// Dynamic dispatch is necessary here because `BleDevice` and
|
||||
// `ClassicDevice` share the `Device` trait (and thus must have
|
||||
// the same return type for `address()` method). This can be
|
||||
// changed if `Device` trait should exclusively define
|
||||
// cross-platform behavior.
|
||||
let classic_addr = match addr {
|
||||
Address::Ble(ble) => ClassicAddress::try_from(ble),
|
||||
Address::Classic(_) => unreachable!(
|
||||
"Address should come from BLE Device, therefore \
|
||||
shouldn't be Classic."
|
||||
),
|
||||
}?;
|
||||
|
||||
let classic_device =
|
||||
bluetooth::new_classic_device(classic_addr).await?;
|
||||
|
||||
match classic_device.pair().await {
|
||||
Ok(_) => {
|
||||
println!("Pairing success!");
|
||||
}
|
||||
Err(err) => println!("Error {}", err),
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("Done scanning");
|
||||
Ok(())
|
||||
};
|
||||
|
||||
executor::block_on(run)
|
||||
|
||||
Reference in New Issue
Block a user