use crate::server::bluetooth_ble_beacon::payload::BeaconFrame;
use anyhow::Result;
pub const UNSUPPORTED_PLATFORM_MESSAGE: &str = concat!(
"BLE beacons require setting manufacturer-specific or service advertising data, which ",
"only Linux/BlueZ can do. On macOS, CBPeripheralManager.startAdvertising: honours only ",
"CBAdvertisementDataLocalNameKey and CBAdvertisementDataServiceUUIDsKey and documents ",
"every other key as ignored, so an iBeacon or Eddystone payload cannot be emitted at all; ",
"Windows advertising is not implemented here. Run bluetooth-ble-beacon on Linux with ",
"bluetoothd running, or use the bluetooth-ble GATT server instead."
);
pub struct BeaconAdvertiser {
#[cfg(target_os = "linux")]
inner: linux::LinuxAdvertiser,
current: Option<BeaconFrame>,
device_name: String,
}
impl BeaconAdvertiser {
#[cfg(target_os = "linux")]
pub async fn open(device_name: String, adapter: Option<String>) -> Result<Self> {
Ok(Self {
inner: linux::LinuxAdvertiser::open(adapter.as_deref()).await?,
current: None,
device_name,
})
}
#[cfg(not(target_os = "linux"))]
pub async fn open(_device_name: String, _adapter: Option<String>) -> Result<Self> {
anyhow::bail!(UNSUPPORTED_PLATFORM_MESSAGE)
}
pub fn adapter_name(&self) -> &str {
#[cfg(target_os = "linux")]
{
self.inner.adapter_name()
}
#[cfg(not(target_os = "linux"))]
{
"none"
}
}
#[cfg(target_os = "linux")]
pub async fn start(&mut self, frame: BeaconFrame) -> Result<()> {
let local_name = frame.fit_local_name(&self.device_name).map(str::to_string);
self.inner.advertise(&frame, local_name).await?;
self.current = Some(frame);
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub async fn start(&mut self, _frame: BeaconFrame) -> Result<()> {
anyhow::bail!(UNSUPPORTED_PLATFORM_MESSAGE)
}
pub async fn stop(&mut self) {
#[cfg(target_os = "linux")]
self.inner.clear();
self.current = None;
}
pub fn current(&self) -> Option<&BeaconFrame> {
self.current.as_ref()
}
pub fn device_name(&self) -> &str {
&self.device_name
}
}
#[cfg(target_os = "linux")]
mod linux {
use super::BeaconFrame;
use anyhow::{Context, Result};
use bluer::adv::{Advertisement, AdvertisementHandle, Type};
use std::collections::{BTreeMap, BTreeSet};
use tracing::{debug, info};
pub struct LinuxAdvertiser {
_session: bluer::Session,
adapter: bluer::Adapter,
adapter_name: String,
handle: Option<AdvertisementHandle>,
}
impl LinuxAdvertiser {
pub async fn open(adapter: Option<&str>) -> Result<Self> {
let session = bluer::Session::new().await.context(
"Failed to connect to BlueZ over D-Bus. Is bluetoothd running \
(systemctl start bluetooth)?",
)?;
let adapter = match adapter {
Some(name) => session
.adapter(name)
.with_context(|| format!("No Bluetooth adapter named {name:?}"))?,
None => session
.default_adapter()
.await
.context("No Bluetooth adapter found")?,
};
adapter
.set_powered(true)
.await
.context("Failed to power on the Bluetooth adapter")?;
let adapter_name = adapter.name().to_string();
info!("BLE beacon bound to adapter {}", adapter_name);
Ok(Self {
_session: session,
adapter,
adapter_name,
handle: None,
})
}
pub fn adapter_name(&self) -> &str {
&self.adapter_name
}
pub async fn advertise(
&mut self,
frame: &BeaconFrame,
local_name: Option<String>,
) -> Result<()> {
self.handle = None;
let mut manufacturer_data = BTreeMap::new();
if let Some((company, data)) = frame.manufacturer_data() {
manufacturer_data.insert(company, data);
}
let mut service_data = BTreeMap::new();
let mut service_uuids = BTreeSet::new();
if let Some((uuid16, data)) = frame.service_data() {
let uuid = uuid16_to_uuid(uuid16);
service_data.insert(uuid, data);
service_uuids.insert(uuid);
}
let advertisement = Advertisement {
advertisement_type: Type::Broadcast,
manufacturer_data,
service_data,
service_uuids,
local_name,
..Default::default()
};
debug!(
"Registering BlueZ advertisement on {}: {}",
self.adapter_name,
frame.describe()
);
let handle = self
.adapter
.advertise(advertisement)
.await
.context("BlueZ refused to register the beacon advertisement")?;
self.handle = Some(handle);
Ok(())
}
pub fn clear(&mut self) {
self.handle = None;
}
}
fn uuid16_to_uuid(uuid16: u16) -> uuid::Uuid {
uuid::Uuid::from_fields(
uuid16 as u32,
0x0000,
0x1000,
&[0x80, 0x00, 0x00, 0x80, 0x5f, 0x9b, 0x34, 0xfb],
)
}
}