Windows BLE 开发指南(Rust windows-rs)

Windows BLE 开发指南(Rust windows-rs)

本演示在 Windows 平台使用 Rust 的 windows-rs 库进行 BLE(低功耗蓝牙)开发:扫描设备、连接与服务发现、选择特征、启用通知(CCCD)、发送与接收数据、断开与清理,同时给出"已配对设备重启"场景的稳态策略


依赖与准备

Cargo.toml 添加依赖:

toml 复制代码
[dependencies]
windows = "0.56"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
uuid = "1"

常用导入(放在你的模块或文件顶部):

rust 复制代码
use windows::{
    core::Result as WinResult,
    Devices::Bluetooth::Advertisement::{
        BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementReceivedEventArgs,
    },
    Devices::Bluetooth::{BluetoothLEDevice, BluetoothConnectionStatus},
    Devices::Bluetooth::GenericAttributeProfile::{
        GattDeviceService, GattCharacteristic, GattCharacteristicProperties,
        GattClientCharacteristicConfigurationDescriptorValue, GattCommunicationStatus,
        GattValueChangedEventArgs, GattProtectionLevel,
    },
    Devices::Enumeration::{
        DeviceInformationCustomPairing, DevicePairingKinds, DevicePairingRequestedEventArgs,
        DevicePairingResultStatus,
    },
    Foundation::TypedEventHandler,
    Storage::Streams::DataReader,
};

辅助函数:UUID 字符串转 Windows GUID:

rust 复制代码
fn guid_from_str(s: &str) -> windows::core::GUID {
    let u = uuid::Uuid::parse_str(s).expect("uuid parse error");
    let (d1, d2, d3, d4) = u.as_fields();
    windows::core::GUID::from_values(d1, d2, d3, *d4)
}

扫描与筛选设备

扫描 5 秒并返回设备列表(MAC 为 12 位十六进制字符串):

rust 复制代码
#[derive(Debug, Clone)]
struct BleDevice { mac: String, name: String, rssi: Option<i16> }

async fn scan_devices(timeout_ms: u64) -> Vec<BleDevice> {
    use std::{collections::HashMap, sync::Arc, time::Duration};
    use tokio::sync::Mutex as AsyncMutex; // 广播事件是异步回调,这里用异步互斥保护聚合表

    // address→设备信息 聚合表(Windows 提供 64 位地址,不是文本 MAC)
    let map = Arc::new(AsyncMutex::new(HashMap::<u64, BleDevice>::new()));
    let watcher = BluetoothLEAdvertisementWatcher::new().unwrap(); // 创建广播监听器
    let map_cb = map.clone();
    // 注册 Received 事件:每条广播提取地址/名称/RSSI 并写入聚合表
    let _token = watcher.Received(&TypedEventHandler::new(
        move |_sender: &Option<BluetoothLEAdvertisementWatcher>, args: &Option<BluetoothLEAdvertisementReceivedEventArgs>| -> WinResult<()> {
            if let Some(args) = args.as_ref() {
                let addr = args.BluetoothAddress()?; // 64 位地址(数值)
                let mac = format!( // 转为 12 位十六进制字符串,便于统一筛选
                    "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
                    (addr >> 40) & 0xff, (addr >> 32) & 0xff, (addr >> 24) & 0xff,
                    (addr >> 16) & 0xff, (addr >> 8) & 0xff, addr & 0xff
                );
                let name = args.Advertisement()?.LocalName()?.to_string(); // 本地名(可能空)
                let rssi = args.RawSignalStrengthInDBm()?; // 信号强度(dBm)
                if let Ok(mut m) = map_cb.try_lock() { // 非阻塞写入,避免回调卡住
                    m.entry(addr).and_modify(|d| { if d.name.is_empty() && !name.is_empty() { d.name = name.clone(); } d.rssi = Some(rssi); })
                        .or_insert(BleDevice { mac, name, rssi: Some(rssi) });
                }
            }
            Ok(())
        }
    )).unwrap();
    watcher.Start().unwrap(); // 开始监听广播
    tokio::time::sleep(Duration::from_millis(timeout_ms)).await;
    watcher.Stop().unwrap(); // 停止监听
    let locked = map.lock().await; // 收敛为列表
    let mut list: Vec<_> = locked.values().cloned().filter(|d| !d.name.is_empty()).collect();
    list.sort_by_key(|d| d.mac.clone());
    list
}

async fn filter_device(mac: &str, list: Vec<BleDevice>) -> Option<BleDevice> {
    // 支持 "AA:BB:..." 或无分隔符/大小写不一致的输入
    let mut s = mac.replace(":", "").replace('-', "").to_lowercase();
    if s.len() != 12 || s.chars().any(|c| !c.is_ascii_hexdigit()) {
        if let Ok(addr_hex) = u64::from_str_radix(&s, 16) { s = format!("{:012x}", addr_hex); }
        else if let Ok(addr_dec) = s.parse::<u64>() { s = format!("{:012x}", addr_dec); }
    }
    list.into_iter().find(|d| d.mac == s)
}

为什么这么做:

  • Windows 广播提供的是数值地址,统一转为 12 位十六进制更利于设备筛选与日志定位。
  • 事件回调中尽量使用 try_lock,避免广播高频导致锁争用。

常见坑:

  • 某些设备广播不带本地名,需允许空名称并在后续才筛掉。
  • 扫描过短可能错过目标设备;根据场景调整 timeout_ms

连接与服务发现

rust 复制代码
async fn connect_and_list_services(device: &BleDevice) -> BluetoothLEDevice {
    let addr = u64::from_str_radix(&device.mac.replace(":", "").to_lowercase(), 16).unwrap(); // 文本 MAC → 数值地址
    let dev = BluetoothLEDevice::FromBluetoothAddressAsync(addr).unwrap().await.unwrap(); // WinRT 异步获取设备对象
    // 等待连接就绪:避免紧接着读服务/写 CCCD 命中未连接状态
    for _ in 0..10 {
        if dev.ConnectionStatus().unwrap() == BluetoothConnectionStatus::Connected { break; }
        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
    }
    // 列出服务(可选):用于确认目标服务是否存在与刷新完成
    let services = dev.GetGattServicesAsync().unwrap().await.unwrap();
    if services.Status().unwrap() == GattCommunicationStatus::Success {
        let list = services.Services().unwrap();
        for s in list.into_iter() { println!("service uuid {:?}", s.Uuid().unwrap()); }
    }
    dev
}
  • 某些设备在建立物理连接后,需要数百毫秒才进入 Connected;过早操作常导致不可达或读空服务。

常见坑:

  • 忽略连接就绪轮询会触发后续"Unreachable"或启用通知中止(E_ABORT)。

特征选择(通知/写入)

按 GUID 过滤,否则枚举回退,并根据属性判定:

rust 复制代码
async fn select_notify(service: &GattDeviceService, guid: windows::core::GUID) -> Option<GattCharacteristic> {
    let res = service.GetCharacteristicsForUuidAsync(guid).unwrap().await.unwrap();
    if res.Status().unwrap() == GattCommunicationStatus::Success {
        let list = res.Characteristics().unwrap();
        for c in list.into_iter() {
            let props = c.CharacteristicProperties().unwrap(); // 判定具备 Notify 或 Indicate
            let ok = (props.0 & GattCharacteristicProperties::Notify.0) != 0 || (props.0 & GattCharacteristicProperties::Indicate.0) != 0;
            if ok { return Some(c); }
        }
    }
    // GUID 过滤失败时,枚举全部特征并匹配 GUID
    let all = service.GetCharacteristicsAsync().unwrap().await.unwrap().Characteristics().unwrap();
    for c in all.into_iter() { if c.Uuid().unwrap() == guid { return Some(c); } }
    None
}

async fn select_write(service: &GattDeviceService, guid: windows::core::GUID) -> Option<GattCharacteristic> {
    let res = service.GetCharacteristicsForUuidAsync(guid).unwrap().await.unwrap();
    if res.Status().unwrap() == GattCommunicationStatus::Success {
        let list = res.Characteristics().unwrap();
        for c in list.into_iter() {
            let p = c.CharacteristicProperties().unwrap(); // 判定具备 Write 或 WriteWithoutResponse
            let ok = (p.0 & GattCharacteristicProperties::Write.0) != 0 || (p.0 & GattCharacteristicProperties::WriteWithoutResponse.0) != 0;
            if ok { return Some(c); }
        }
    }
    let all = service.GetCharacteristicsAsync().unwrap().await.unwrap().Characteristics().unwrap();
    for c in all.into_iter() { if c.Uuid().unwrap() == guid { return Some(c); } }
    None
}
  • 设备端在刷新期间可能返回空列表,枚举回退能避免漏选;属性判定可避免误选不可用特征。

常见坑:

  • 仅按 GUID 命中但属性不满足(无 Notify/Write),后续启用或写入会失败。

启用通知(CCCD)与回调注册

rust 复制代码
async fn enable_notify_with_retry(ch: &GattCharacteristic) -> bool {
    tokio::time::sleep(std::time::Duration::from_millis(1000)).await; // 预延时,避开设备忙/栈刷新
    for _ in 0..3 {
        if let Ok(op) = ch.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue::Notify) {
            if let Ok(status) = op.await { if status == GattCommunicationStatus::Success { return true; } } // 首选 Notify
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }
    for _ in 0..2 {
        if let Ok(op) = ch.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue::Indicate) {
            if let Ok(status) = op.await { if status == GattCommunicationStatus::Success { return true; } } // 回退 Indicate
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }
    false
}

fn register_value_changed(ch: &GattCharacteristic, mut on_notify: impl FnMut(Vec<u8>) + Send + 'static) {
    let handler = TypedEventHandler::<GattCharacteristic, GattValueChangedEventArgs>::new(
        move |_sender: &Option<GattCharacteristic>, args: &Option<GattValueChangedEventArgs>| {
            if let Some(args) = args.as_ref() {
                if let Ok(buf) = args.CharacteristicValue() {
                    if let Ok(reader) = DataReader::FromBuffer(&buf) { // IBuffer → Vec<u8>
                        if let Ok(len) = buf.Length() {
                            let mut data = vec![0u8; len as usize];
                            let _ = reader.ReadBytes(&mut data);
                            on_notify(data);
                        }
                    }
                }
            }
            Ok(())
        }
    );
    let _ = ch.ValueChanged(&handler);
}
  • Notify 不需要 ACK,实时性好;设备只支持 Indicate 时需回退。
  • IBuffer 转字节后交给上层解析,避免 WinRT 读取阻塞。

常见坑:

  • 启用通知过早会被中止(E_ABORT);加延时与重试能显著降低概率。

写入(带响应优先,失败回退无响应)

rust 复制代码
async fn write_with_result_and_fallback(ch: &GattCharacteristic, data: &[u8]) -> GattCommunicationStatus {
    use windows::Storage::Streams::{InMemoryRandomAccessStream, DataWriter};
    let stream = InMemoryRandomAccessStream::new().unwrap(); // 构造内存流
    let out = stream.GetOutputStreamAt(0).unwrap(); // 取输出流
    let writer = DataWriter::CreateDataWriter(&out).unwrap(); // 创建写入器
    writer.WriteBytes(data).unwrap(); // 写入字节
    let buf = writer.DetachBuffer().unwrap(); // 拆出 IBuffer

    let res = ch.WriteValueWithResultAsync(&buf).unwrap().await.unwrap(); // 带响应写入
    let status = res.Status().unwrap(); // 读取通信状态(可结合 ProtocolError 定位 ATT 错误)
    if status != GattCommunicationStatus::Success {
        let fb = ch.WriteValueAsync(&buf).unwrap().await.unwrap(); // 回退:无响应写入
        return fb;
    }
    status
}

为什么这么做:

  • 带响应写可获 ATT 错码(权限/长度/不允许等);设备仅支持无响应写时自动回退。

常见坑:

  • 未加密/未配对时常见 Insufficient Authentication;需先建立加密会话。

断开与清理(顺序至关重要)

rust 复制代码
async fn disconnect_cleanup(dev: &BluetoothLEDevice, notify_char: &GattCharacteristic, token: windows::Foundation::EventRegistrationToken) {
    let _ = notify_char.RemoveValueChanged(token); // 先移除通知事件,避免回调残留
    if let Ok(op) = notify_char.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue::None) { let _ = op.await; } // 关闭订阅(CCCD=None)
    if let Ok(svc) = notify_char.Service() { if let Ok(sess) = svc.Session() { let _ = sess.SetMaintainConnection(false); } } // 取消保持连接
    let _ = dev.Close(); // 关闭设备句柄
}
  • 不正确的清理顺序会导致下次连接启用通知失败或会话不可达。

常见坑:

  • 忘记 RemoveValueChanged 导致 ValueChanged 持有对象,写入 None 时报错。

已配对设备重启的稳态策略(清理 + 重试)

设备已在系统层"配对且连接",当设备重启进入配对模式时,Windows 保留旧连接/GATT 缓存,常见错误:

  • "notify characteristic not found"
  • "enable notify/indicate failed" 或 E_ABORT(0x80004004)

建议在连接前执行 OS 级清理:

rust 复制代码
async fn unpair_and_close(address: u64) {
    let dev = BluetoothLEDevice::FromBluetoothAddressAsync(address).unwrap().await.unwrap();
    if let Ok(di) = dev.DeviceInformation() {
        if let Ok(pairing) = di.Pairing() {
            if pairing.IsPaired().unwrap_or(false) {
                let _ = pairing.UnpairAsync().unwrap().await;
            }
        }
    }
    let _ = dev.Close();
    tokio::time::sleep(std::time::Duration::from_millis(800)).await;
}

随后再进行连接、服务发现、特征选择与 CCCD 启用,并在各步骤加入适度延时与重试。


组合示例:连接→订阅→发送→接收→断开

rust 复制代码
#[tokio::main]
async fn main() {
    let list = scan_devices(5000).await;
    let dev = filter_device("208B37997529", list).expect("device not found");

    let addr = u64::from_str_radix(&dev.mac, 16).unwrap();
    unpair_and_close(addr).await; // 已配对场景建议先清理

    let device = connect_and_list_services(&dev).await;
    let service = {
        let guid = guid_from_str("01000100-0000-1000-8000-009078563412");
        let list = device.GetGattServicesAsync().unwrap().await.unwrap().Services().unwrap();
        list.into_iter().find(|s| s.Uuid().unwrap() == guid).expect("service not found")
    };

    tokio::time::sleep(std::time::Duration::from_millis(800)).await;
    let notify = select_notify(&service, guid_from_str("02000200-0000-1000-8000-009178563412")).expect("notify not found");
    let writec = select_write(&service, guid_from_str("03000300-0000-1000-8000-009278563412")).expect("write not found");

    let _ = notify.SetProtectionLevel(GattProtectionLevel::EncryptionRequired);
    let _ = writec.SetProtectionLevel(GattProtectionLevel::EncryptionRequired);

    if !enable_notify_with_retry(&notify).await { panic!("enable notify failed"); }
    register_value_changed(&notify, |data| { println!("notify: {} bytes", data.len()); });

    let payload = b"example payload";
    let status = write_with_result_and_fallback(&writec, payload).await;
    println!("write status: {:?}", status);

    // 清理
    // 注意:真实项目中保存 ValueChanged 注册的 token,并在断开时传入
    let dummy_token = windows::Foundation::EventRegistrationToken { value: 0 };
    disconnect_cleanup(&device, &notify, dummy_token).await;
}

故障排查与最佳实践

  • WinRT await 与并发:将涉及 WinRT await 的代码放到阻塞线程或在当前线程执行,避免 Send 约束问题
  • 连接就绪:轮询 BluetoothConnectionStatus::Connected 再进行特征与 CCCD 操作
  • 特征与 CCCD:获取服务后延时、特征选择重试;CCCD 启用延时、Notify→Indicate 回退;必要时重新抓取一次特征再启用
  • 已配对设备重启:务必先执行 UnpairAsync + Close + 延时 再连接,显著降低缓存不一致导致的失败
  • 断开顺序:移除事件→CCCD=None→取消保持连接→Close 设备;不当的顺序会让下次连接启用通知失败

小结

本文给出了一套完整的、可直接复制的 Windows BLE 开发代码片段与操作步骤,涵盖扫描、连接与服务发现、特征选择、启用通知、写入与接收、断开清理,以及已配对设备重启场景的稳态策略。将这些片段按需组合,即可搭建稳定的 BLE 通信链路。


带详注的代码片段(逐行说明)

1) 扫描与筛选(详注版)

rust 复制代码
use windows::{
    core::Result as WinResult,
    Devices::Bluetooth::Advertisement::{
        BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementReceivedEventArgs,
    },
    Foundation::TypedEventHandler,
};
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::sync::Mutex as AsyncMutex;

#[derive(Debug, Clone)]
struct BleDevice { mac: String, name: String, rssi: Option<i16> }

// 扫描指定时长,聚合设备到列表
async fn scan_devices(timeout_ms: u64) -> Vec<BleDevice> {
    // 使用共享 HashMap 聚合:Windows 广播给出的是 64 位地址(非文本 MAC)
    let map = Arc::new(AsyncMutex::new(HashMap::<u64, BleDevice>::new()));

    // 创建广播监听器
    let watcher = BluetoothLEAdvertisementWatcher::new().unwrap();
    let map_cb = map.clone();

    // 注册接收事件:每条广播中提取地址、设备名与 RSSI
    let _token = watcher.Received(&TypedEventHandler::new(
        move |_sender: &Option<BluetoothLEAdvertisementWatcher>, args: &Option<BluetoothLEAdvertisementReceivedEventArgs>| -> WinResult<()> {
            if let Some(args) = args.as_ref() {
                // 设备地址为 64 位整型,转为 12 位十六进制字符串(不含分隔符)
                let addr = args.BluetoothAddress()?;
                let mac = format!(
                    "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
                    (addr >> 40) & 0xff, (addr >> 32) & 0xff, (addr >> 24) & 0xff,
                    (addr >> 16) & 0xff, (addr >> 8) & 0xff, addr & 0xff
                );
                // 广告包中的本地名
                let name = args.Advertisement()?.LocalName()?.to_string();
                // 信号强度(可能不存在)
                let rssi = args.RawSignalStrengthInDBm()?;

                // 聚合到共享表:若已有记录则更新更有价值的信息(非空名称、最新 RSSI)
                if let Ok(mut m) = map_cb.try_lock() {
                    m.entry(addr)
                        .and_modify(|d| { if d.name.is_empty() && !name.is_empty() { d.name = name.clone(); } d.rssi = Some(rssi); })
                        .or_insert(BleDevice { mac, name, rssi: Some(rssi) });
                }
            }
            Ok(())
        }
    )).unwrap();

    // 开始监听指定时间窗口
    watcher.Start().unwrap();
    tokio::time::sleep(Duration::from_millis(timeout_ms)).await;
    watcher.Stop().unwrap();

    // 收敛为列表并按 MAC 排序,过滤空名称
    let locked = map.lock().await;
    let mut list: Vec<_> = locked.values().cloned().filter(|d| !d.name.is_empty()).collect();
    list.sort_by_key(|d| d.mac.clone());
    list
}

// 按 MAC 文本筛选设备:支持去分隔符与大小写统一,兼容十六/十进制
async fn filter_device(mac: &str, list: Vec<BleDevice>) -> Option<BleDevice> {
    let mut s = mac.replace(":", "").replace('-', "").to_lowercase();
    if s.len() != 12 || s.chars().any(|c| !c.is_ascii_hexdigit()) {
        if let Ok(addr_hex) = u64::from_str_radix(&s, 16) { s = format!("{:012x}", addr_hex); }
        else if let Ok(addr_dec) = s.parse::<u64>() { s = format!("{:012x}", addr_dec); }
    }
    list.into_iter().find(|d| d.mac == s)
}

2) 连接与服务发现(详注版)

rust 复制代码
use windows::Devices::Bluetooth::{BluetoothLEDevice, BluetoothConnectionStatus};
use windows::Devices::Bluetooth::GenericAttributeProfile::{GattCommunicationStatus};

// 建立到设备的连接,并打印服务列表(用于诊断)
async fn connect_and_list_services(device: &BleDevice) -> BluetoothLEDevice {
    // 文本 MAC → 64 位地址(十六进制解析)
    let addr = u64::from_str_radix(&device.mac.replace(":", "").to_lowercase(), 16).unwrap();
    // 异步获取设备对象(WinRT)
    let dev = BluetoothLEDevice::FromBluetoothAddressAsync(addr).unwrap().await.unwrap();

    // 连接就绪等待:部分设备需要一点时间进入 Connected 状态
    for _ in 0..10 {
        if dev.ConnectionStatus().unwrap() == BluetoothConnectionStatus::Connected { break; }
        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
    }

    // 枚举 GATT 服务并打印 UUID(便于确认服务是否刷新与存在)
    let services = dev.GetGattServicesAsync().unwrap().await.unwrap();
    if services.Status().unwrap() == GattCommunicationStatus::Success {
        let list = services.Services().unwrap();
        for s in list.into_iter() { println!("service uuid {:?}", s.Uuid().unwrap()); }
    }
    dev
}

3) 特征选择(详注版)

rust 复制代码
use windows::Devices::Bluetooth::GenericAttributeProfile::{
    GattDeviceService, GattCharacteristic, GattCharacteristicProperties, GattCommunicationStatus,
};

// 选择通知特征:先 GUID 过滤 + 按属性检查;失败枚举全部回退
async fn select_notify(service: &GattDeviceService, guid: windows::core::GUID) -> Option<GattCharacteristic> {
    let res = service.GetCharacteristicsForUuidAsync(guid).unwrap().await.unwrap();
    if res.Status().unwrap() == GattCommunicationStatus::Success {
        let list = res.Characteristics().unwrap();
        for c in list.into_iter() {
            let props = c.CharacteristicProperties().unwrap();
            let ok = (props.0 & GattCharacteristicProperties::Notify.0) != 0 || (props.0 & GattCharacteristicProperties::Indicate.0) != 0;
            if ok { return Some(c); }
        }
    }
    // 枚举回退:某些设备在刷新期间 GUID 过滤可能返回空
    let all = service.GetCharacteristicsAsync().unwrap().await.unwrap().Characteristics().unwrap();
    for c in all.into_iter() { if c.Uuid().unwrap() == guid { return Some(c); } }
    None
}

// 选择写特征:同理,需具备 Write 或 WriteWithoutResponse 属性
async fn select_write(service: &GattDeviceService, guid: windows::core::GUID) -> Option<GattCharacteristic> {
    let res = service.GetCharacteristicsForUuidAsync(guid).unwrap().await.unwrap();
    if res.Status().unwrap() == GattCommunicationStatus::Success {
        let list = res.Characteristics().unwrap();
        for c in list.into_iter() {
            let p = c.CharacteristicProperties().unwrap();
            let ok = (p.0 & GattCharacteristicProperties::Write.0) != 0 || (p.0 & GattCharacteristicProperties::WriteWithoutResponse.0) != 0;
            if ok { return Some(c); }
        }
    }
    let all = service.GetCharacteristicsAsync().unwrap().await.unwrap().Characteristics().unwrap();
    for c in all.into_iter() { if c.Uuid().unwrap() == guid { return Some(c); } }
    None
}

4) 启用通知与注册回调(详注版)

rust 复制代码
use windows::Devices::Bluetooth::GenericAttributeProfile::{
    GattCharacteristic, GattClientCharacteristicConfigurationDescriptorValue, GattCommunicationStatus,
};
use windows::Devices::Bluetooth::GenericAttributeProfile::{GattValueChangedEventArgs};
use windows::Storage::Streams::DataReader;
use windows::Foundation::TypedEventHandler;

// 启用通知:优先 Notify,失败回退 Indicate;加入预延时与重试以跨过设备刷新窗口
async fn enable_notify_with_retry(ch: &GattCharacteristic) -> bool {
    // 预延时:避免立即写 CCCD 命中设备忙或栈刷新
    tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
    // 尝试 Notify 多次
    for _ in 0..3 {
        if let Ok(op) = ch.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue::Notify) {
            if let Ok(status) = op.await { if status == GattCommunicationStatus::Success { return true; } }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }
    // 回退 Indicate
    for _ in 0..2 {
        if let Ok(op) = ch.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue::Indicate) {
            if let Ok(status) = op.await { if status == GattCommunicationStatus::Success { return true; } }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }
    false
}

// 注册通知回调:将 IBuffer 转为 Vec<u8> 并交给调用者处理
fn register_value_changed(ch: &GattCharacteristic, mut on_notify: impl FnMut(Vec<u8>) + Send + 'static) {
    let handler = TypedEventHandler::<GattCharacteristic, GattValueChangedEventArgs>::new(
        move |_sender: &Option<GattCharacteristic>, args: &Option<GattValueChangedEventArgs>| {
            if let Some(args) = args.as_ref() {
                if let Ok(buf) = args.CharacteristicValue() {
                    if let Ok(reader) = DataReader::FromBuffer(&buf) {
                        if let Ok(len) = buf.Length() {
                            let mut data = vec![0u8; len as usize];
                            let _ = reader.ReadBytes(&mut data);
                            on_notify(data);
                        }
                    }
                }
            }
            Ok(())
        }
    );
    let _ = ch.ValueChanged(&handler);
}

5) 写入(详注版)

rust 复制代码
use windows::Devices::Bluetooth::GenericAttributeProfile::{GattCharacteristic, GattCommunicationStatus};
use windows::Storage::Streams::{InMemoryRandomAccessStream, DataWriter};

// 写入封装:优先带响应写入(便于获取协议错误),失败回退无响应
async fn write_with_result_and_fallback(ch: &GattCharacteristic, data: &[u8]) -> GattCommunicationStatus {
    // WinRT 写入 API 需要 IBuffer;这里通过内存流 + DataWriter 构造缓冲区
    let stream = InMemoryRandomAccessStream::new().unwrap();
    let out = stream.GetOutputStreamAt(0).unwrap();
    let writer = DataWriter::CreateDataWriter(&out).unwrap();
    writer.WriteBytes(data).unwrap();
    let buf = writer.DetachBuffer().unwrap();

    // 带响应写入:可读取状态与协议错误码(ATT),便于定位权限或长度问题
    let res = ch.WriteValueWithResultAsync(&buf).unwrap().await.unwrap();
    let status = res.Status().unwrap();
    if status != GattCommunicationStatus::Success {
        // 回退为无响应写:部分设备仅允许无响应写
        let fb = ch.WriteValueAsync(&buf).unwrap().await.unwrap();
        return fb;
    }
    status
}

6) 断开与清理(详注版)

rust 复制代码
use windows::Devices::Bluetooth::BluetoothLEDevice;
use windows::Devices::Bluetooth::GenericAttributeProfile::{GattCharacteristic, GattClientCharacteristicConfigurationDescriptorValue};

// 断开顺序:RemoveValueChanged → CCCD=None → MaintainConnection(false) → Close
async fn disconnect_cleanup(dev: &BluetoothLEDevice, notify_char: &GattCharacteristic, token: windows::Foundation::EventRegistrationToken) {
    let _ = notify_char.RemoveValueChanged(token);
    if let Ok(op) = notify_char.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue::None) { let _ = op.await; }
    if let Ok(svc) = notify_char.Service() { if let Ok(sess) = svc.Session() { let _ = sess.SetMaintainConnection(false); } }
    let _ = dev.Close();
}

7) 已配对设备重启的清理(详注版)

rust 复制代码
use windows::Devices::Bluetooth::BluetoothLEDevice;

// 在已配对且系统持有旧连接的情况下:先 Unpair + Close 再连接,降低缓存不一致导致的失败
async fn unpair_and_close(address: u64) {
    let dev = BluetoothLEDevice::FromBluetoothAddressAsync(address).unwrap().await.unwrap();
    if let Ok(di) = dev.DeviceInformation() {
        if let Ok(pairing) = di.Pairing() {
            if pairing.IsPaired().unwrap_or(false) {
                let _ = pairing.UnpairAsync().unwrap().await; // 解除配对,释放旧权限与密钥
            }
        }
    }
    let _ = dev.Close(); // 关闭旧设备句柄
    tokio::time::sleep(std::time::Duration::from_millis(800)).await; // 等待栈刷新
}

8) 组合流程(详注版)

rust 复制代码
#[tokio::main]
async fn main() {
    // 1) 扫描并选择目标设备
    let list = scan_devices(5000).await;
    let dev = filter_device("208B37997529", list).expect("device not found");

    // 2) 已配对场景建议先清理旧状态(Unpair + Close + 延时)
    let addr = u64::from_str_radix(&dev.mac, 16).unwrap();
    unpair_and_close(addr).await;

    // 3) 连接并输出服务列表(诊断用途)
    let device = connect_and_list_services(&dev).await;

    // 4) 获取目标服务(按 GUID 匹配)
    let service = {
        let guid = guid_from_str("01000100-0000-1000-8000-009078563412");
        let list = device.GetGattServicesAsync().unwrap().await.unwrap().Services().unwrap();
        list.into_iter().find(|s| s.Uuid().unwrap() == guid).expect("service not found")
    };

    // 5) 特征选择前短暂等待,随后选择通知与写特征
    tokio::time::sleep(std::time::Duration::from_millis(800)).await;
    let notify = select_notify(&service, guid_from_str("02000200-0000-1000-8000-009178563412")).expect("notify not found");
    let writec = select_write(&service, guid_from_str("03000300-0000-1000-8000-009278563412")).expect("write not found");

    // 6) 若设备要求加密,设置保护级别为加密
    let _ = notify.SetProtectionLevel(windows::Devices::Bluetooth::GenericAttributeProfile::GattProtectionLevel::EncryptionRequired);
    let _ = writec.SetProtectionLevel(windows::Devices::Bluetooth::GenericAttributeProfile::GattProtectionLevel::EncryptionRequired);

    // 7) 启用通知(带重试/回退),并注册通知回调
    if !enable_notify_with_retry(&notify).await { panic!("enable notify failed"); }
    register_value_changed(&notify, |data| { println!("notify: {} bytes", data.len()); });

    // 8) 写入示例负载(带响应优先,失败回退)
    let payload = b"example payload";
    let status = write_with_result_and_fallback(&writec, payload).await;
    println!("write status: {:?}", status);

    // 9) 断开与清理(真实项目中保存并传入 ValueChanged 的 token)
    let dummy_token = windows::Foundation::EventRegistrationToken { value: 0 };
    disconnect_cleanup(&device, &notify, dummy_token).await;
}
相关推荐
醉方休38 分钟前
Webpack loader 的执行机制
前端·webpack·rust
前端老宋Running1 小时前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔1 小时前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户4445543654261 小时前
Android的自定义View
前端
WILLF1 小时前
HTML iframe 标签
前端·javascript
枫,为落叶1 小时前
Axios使用教程(一)
前端
小章鱼学前端1 小时前
2025 年最新 Fabric.js 实战:一个完整可上线的图片选区标注组件(含全部源码).
前端·vue.js
ohyeah1 小时前
JavaScript 词法作用域、作用域链与闭包:从代码看机制
前端·javascript
流星稍逝1 小时前
手搓一个简简单单进度条
前端