RustとRaspberry Pi Pico WでBLEをスキャンするからの続きものだったり。
前回まででBLEのペリフェラルをスキャンすることが出来ました。今回はそのスキャン結果を解析してみます。
この記事での目標は、デバイス名と16-bit UUIDを取得することです。
環境
- Raspberry Pi Pico W
- Rust 1.83.0
Advertising Data
Advertising Dataは以下のような構造が繰り返し出現する形式です。
: 1 byte : Length byte :
+---------+---------+--------+--------+---------+---------+--------+--------+
| Length | Data | Length | Data | ...
+---------+---------+--------+--------+---------+---------+--------+--------+
: 1 byte : Length - 1 byte :
+---------+--------+--------+
| AD Type | AD Data |
+---------+--------+--------+
- Length: 1 byte
- AD Type: 1 byte
- AD Data: Length - 1 byte
AD Type
AD Typeはそれなりの種類がありますので、ここではこの記事で使うものだけを紹介します。
- 0x03: Complete List of 16-bit Service or Service Class UUIDs
- 0x09: Complete Local Name
その他については、Assigned Numbersを参照してください。
パース
pub struct AssignedNumber;
impl AssignedNumber {
pub const COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: u8 = 0x03;
pub const COMPLETE_LOCAL_NAME: u8 = 0x09;
}
pub struct AdvData<'a> {
pub name: Option<&'a str>,
pub uuid: Option<&'a [u8]>,
}
impl AdvData<'_> {
pub fn parse(data: &[u8]) -> AdvData {
let mut name = None;
let mut uuid = None;
let mut i = 0;
while i < data.len() {
let length = data[i] as usize;
if length == 0 || i + 1 + length > data.len() {
break;
}
let ad_type = data[i + 1];
let ad_data = &data[i + 2..=i + length];
match ad_type {
AssignedNumber::COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS => {
uuid = Some(ad_data);
}
AssignedNumber::COMPLETE_LOCAL_NAME => {
name = Some(core::str::from_utf8(ad_data).unwrap());
}
_ => {}
}
i += 1 + length;
}
AdvData { name, uuid }
}
}
実装は至ってシンプルです。
データの先頭から順にAD Typeを見ていき、それに応じてデータを取得しています。
スキャン
use bt_hci::cmd::le::{LeSetScanEnable, LeSetScanParams};
use bt_hci::controller::ControllerCmdSync;
use embassy_futures::join::join;
use trouble_host::prelude::*;
use crate::adv::AdvData;
/// Size of L2CAP packets
const L2CAP_MTU: usize = 128;
/// Max number of connections
const CONNECTIONS_MAX: usize = 1;
/// Max number of L2CAP channels.
const L2CAP_CHANNELS_MAX: usize = 3; // Signal + att + CoC
type Resources<C> = HostResources<C, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX, L2CAP_MTU>;
pub async fn scan<C>(controller: C)
where
C: Controller + ControllerCmdSync<LeSetScanParams> + ControllerCmdSync<LeSetScanEnable>,
{
let mut resources = Resources::new(PacketQos::None);
let (_stack, _, mut central, mut runner) =
trouble_host::new(controller, &mut resources).build();
let config = ScanConfig {
..Default::default()
};
log::info!("Scanning for peripheral...");
let _ = join(runner.run(), async {
loop {
let result = central.scan(&config).await.unwrap();
for report in result.iter() {
report
.map(|report| {
let adv_data = AdvData::parse(&report.data);
if let Some(name) = adv_data.name {
log::info!("Peripheral found: {}", name);
}
})
.ok();
}
}
})
.await;
}
スキャン結果を取得して、AdvData
にパースしています。
デバイス名が取得できていることが確認できます。
UUID
UUIDには事前に割り当てられた範囲が存在しており、Bluetooth_Base_UUIDと呼ばれています。
Bluetooth_Base_UUIDの値は00000000-0000-1000-8000-00805F9B34FB
です。
この範囲のエイリアスとして、16bitと32bitのUUIDが存在しています。
これらは以下のアルゴリズムで128bitのUUIDに変換できます。
16bitのUUIDを取得する場合は、uuid
をu16
に変換してから上記のアルゴリズムを適用します。
use core::fmt;
const BLUETOOTH_BASE_UUID: u128 = 0x00000000_0000_1000_8000_00805F9B34FB;
pub struct Uuid(pub u128);
impl fmt::Debug for Uuid {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
(self.0 >> 96) as u32,
(self.0 >> 80) as u16,
(self.0 >> 64) as u16,
(self.0 >> 48) as u16,
self.0 as u64
)
}
}
impl From<u128> for Uuid {
fn from(value: u128) -> Self {
Uuid(value)
}
}
impl From<u32> for Uuid {
fn from(value: u32) -> Self {
let uuid = (value as u128) << 96 | BLUETOOTH_BASE_UUID;
Uuid(uuid)
}
}
impl From<u16> for Uuid {
fn from(value: u16) -> Self {
let uuid = (value as u128) << 96 | BLUETOOTH_BASE_UUID;
Uuid(uuid)
}
}
use crate::uuid::Uuid;
...
pub struct AdvData<'a> {
pub name: Option<&'a str>,
pub uuid: Option<&'a [u8]>,
pub uuid: Option<Uuid>,
}
impl AdvData<'_> {
...
match ad_type {
AssignedNumber::COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS => {
if ad_data.len() >= 2 {
uuid = Some(ad_data);
let value = u16::from_le_bytes([ad_data[0], ad_data[1]]);
uuid = Some(value.into());
}
}
AssignedNumber::COMPLETE_LOCAL_NAME => {
name = Some(core::str::from_utf8(ad_data).unwrap());
}
_ => {}
}
...
}
...
log::info!("Scanning for peripheral...");
let _ = join(runner.run(), async {
loop {
let result = central.scan(&config).await.unwrap();
for report in result.iter() {
report
.map(|report| {
let adv_data = AdvData::parse(&report.data);
if let Some(name) = adv_data.name {
log::info!("Peripheral found: {}", name);
}
match (adv_data.name, adv_data.uuid) {
(Some(name), Some(uuid)) => {
log::info!("Peripheral found: {} {:?}", name, uuid);
}
_ => {}
}
})
.ok();
}
}
})
.await;
...
UUIDも取得できるようになりました。