[fp-rs] Implemented classic device pairing

This commit is contained in:
Lucas Silva Shepard
2023-07-29 17:18:46 -07:00
parent 9da0bf2987
commit fc07423925
8 changed files with 242 additions and 26 deletions
+1
View File
@@ -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 -3
View File
@@ -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>;
}
+15 -1
View File
@@ -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),
}
+9 -3
View File
@@ -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
}
+132 -12
View File
@@ -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 {
+37 -1
View File
@@ -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.")),
}
}
}
+35 -5
View File
@@ -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)