blob: a2107adadf16db1073461f6e57b0530ac93bd291 [file] [log] [blame]
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::packets::link_layer::LegacyAdvertisingType;
use netsim_proto::model::chip::ble_beacon::{
advertise_settings::{
AdvertiseMode as Mode, AdvertiseTxPower as Level, Interval as IntervalProto,
Tx_power as TxPowerProto,
},
AdvertiseSettings as AdvertiseSettingsProto,
};
use std::time::Duration;
// From packages/modules/Bluetooth/framework/java/android/bluetooth/le/BluetoothLeAdvertiser.java#151
const MODE_LOW_POWER_MS: u64 = 1000;
const MODE_BALANCED_MS: u64 = 250;
const MODE_LOW_LATENCY_MS: u64 = 100;
// From packages/modules/Bluetooth/framework/java/android/bluetooth/le/BluetoothLeAdvertiser.java#159
const TX_POWER_ULTRA_LOW_DBM: i8 = -21;
const TX_POWER_LOW_DBM: i8 = -15;
const TX_POWER_MEDIUM_DBM: i8 = -7;
const TX_POWER_HIGH_DBM: i8 = 1;
/// Configurable settings for ble beacon advertisements.
#[derive(Debug, PartialEq)]
pub struct AdvertiseSettings {
/// Time interval between advertisements.
pub mode: AdvertiseMode,
/// Transmit power level for advertisements and scan responses.
pub tx_power_level: TxPowerLevel,
/// Whether the beacon will respond to scan requests.
pub scannable: bool,
/// How long to send advertisements for before stopping.
pub timeout: Option<Duration>,
}
impl AdvertiseSettings {
/// Returns a new advertise settings builder with no fields.
pub fn builder() -> AdvertiseSettingsBuilder {
AdvertiseSettingsBuilder::default()
}
/// Returns a new advertise settings with fields from a protobuf.
pub fn from_proto(proto: &AdvertiseSettingsProto) -> Result<Self, String> {
proto.try_into()
}
/// Returns the PDU type of advertise packets with the provided settings
pub fn get_packet_type(&self) -> LegacyAdvertisingType {
if self.scannable {
LegacyAdvertisingType::AdvScanInd
} else {
LegacyAdvertisingType::AdvNonconnInd
}
}
}
impl TryFrom<&AdvertiseSettingsProto> for AdvertiseSettings {
type Error = String;
fn try_from(value: &AdvertiseSettingsProto) -> Result<Self, Self::Error> {
let mut builder = AdvertiseSettingsBuilder::default();
if let Some(mode) = value.interval.as_ref() {
builder.mode(mode.into());
}
if let Some(tx_power) = value.tx_power.as_ref() {
builder.tx_power_level(tx_power.try_into()?);
}
if value.scannable {
builder.scannable();
}
if value.timeout != u64::default() {
builder.timeout(Duration::from_millis(value.timeout));
}
Ok(builder.build())
}
}
impl TryFrom<&AdvertiseSettings> for AdvertiseSettingsProto {
type Error = String;
fn try_from(value: &AdvertiseSettings) -> Result<Self, Self::Error> {
Ok(AdvertiseSettingsProto {
interval: Some(value.mode.try_into()?),
tx_power: Some(value.tx_power_level.into()),
scannable: value.scannable,
timeout: value.timeout.unwrap_or_default().as_millis().try_into().map_err(|_| {
String::from("could not convert timeout to millis: must fit in a u64")
})?,
..Default::default()
})
}
}
#[derive(Default)]
/// Builder for BLE beacon advertise settings.
pub struct AdvertiseSettingsBuilder {
mode: Option<AdvertiseMode>,
tx_power_level: Option<TxPowerLevel>,
scannable: bool,
timeout: Option<Duration>,
}
impl AdvertiseSettingsBuilder {
/// Returns a new advertise settings builder with empty fields.
pub fn new() -> Self {
Self::default()
}
/// Build the advertise settings.
pub fn build(&self) -> AdvertiseSettings {
AdvertiseSettings {
mode: self.mode.unwrap_or_default(),
tx_power_level: self.tx_power_level.unwrap_or_default(),
scannable: self.scannable,
timeout: self.timeout,
}
}
/// Set the advertise mode.
pub fn mode(&mut self, mode: AdvertiseMode) -> &mut Self {
self.mode = Some(mode);
self
}
/// Set the transmit power level.
pub fn tx_power_level(&mut self, tx_power_level: TxPowerLevel) -> &mut Self {
self.tx_power_level = Some(tx_power_level);
self
}
/// Set whether the beacon will respond to scan requests.
pub fn scannable(&mut self) -> &mut Self {
self.scannable = true;
self
}
/// Set how long the beacon will send advertisements for.
pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
self.timeout = Some(timeout);
self
}
}
/// A BLE beacon advertise mode. Can be casted to/from a protobuf message.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct AdvertiseMode {
/// The time interval between advertisements.
pub interval: Duration,
}
impl AdvertiseMode {
/// Create an `AdvertiseMode` from an `std::time::Duration` representing the time interval between advertisements.
pub fn new(interval: Duration) -> Self {
AdvertiseMode { interval }
}
}
impl Default for AdvertiseMode {
fn default() -> Self {
Self { interval: Duration::from_millis(MODE_LOW_POWER_MS) }
}
}
impl From<&IntervalProto> for AdvertiseMode {
fn from(value: &IntervalProto) -> Self {
Self {
interval: Duration::from_millis(match value {
IntervalProto::Milliseconds(ms) => *ms,
IntervalProto::AdvertiseMode(mode) => match mode.enum_value_or_default() {
Mode::LOW_POWER => MODE_LOW_POWER_MS,
Mode::BALANCED => MODE_BALANCED_MS,
Mode::LOW_LATENCY => MODE_LOW_LATENCY_MS,
},
_ => MODE_LOW_POWER_MS,
}),
}
}
}
impl TryFrom<AdvertiseMode> for IntervalProto {
type Error = String;
fn try_from(value: AdvertiseMode) -> Result<Self, Self::Error> {
Ok(
match value.interval.as_millis().try_into().map_err(|_| {
String::from("failed to convert interval: duration as millis must fit in a u64")
})? {
MODE_LOW_POWER_MS => IntervalProto::AdvertiseMode(Mode::LOW_POWER.into()),
MODE_BALANCED_MS => IntervalProto::AdvertiseMode(Mode::BALANCED.into()),
MODE_LOW_LATENCY_MS => IntervalProto::AdvertiseMode(Mode::LOW_LATENCY.into()),
ms => IntervalProto::Milliseconds(ms),
},
)
}
}
/// A BLE beacon transmit power level. Can be casted to/from a protobuf message.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct TxPowerLevel {
/// The transmit power in dBm.
pub dbm: i8,
}
impl TxPowerLevel {
/// Create a `TxPowerLevel` from an `i8` measuring power in dBm.
pub fn new(dbm: i8) -> Self {
TxPowerLevel { dbm }
}
}
impl Default for TxPowerLevel {
fn default() -> Self {
TxPowerLevel { dbm: TX_POWER_LOW_DBM }
}
}
impl TryFrom<&TxPowerProto> for TxPowerLevel {
type Error = String;
fn try_from(value: &TxPowerProto) -> Result<Self, Self::Error> {
Ok(Self {
dbm: (match value {
TxPowerProto::Dbm(dbm) => (*dbm)
.try_into()
.map_err(|_| "failed to convert tx power level: it must fit in an i8")?,
TxPowerProto::TxPowerLevel(level) => match level.enum_value_or_default() {
Level::ULTRA_LOW => TX_POWER_ULTRA_LOW_DBM,
Level::LOW => TX_POWER_LOW_DBM,
Level::MEDIUM => TX_POWER_MEDIUM_DBM,
Level::HIGH => TX_POWER_HIGH_DBM,
},
_ => TX_POWER_LOW_DBM,
}),
})
}
}
impl From<TxPowerLevel> for TxPowerProto {
fn from(value: TxPowerLevel) -> Self {
match value.dbm {
TX_POWER_ULTRA_LOW_DBM => TxPowerProto::TxPowerLevel(Level::ULTRA_LOW.into()),
TX_POWER_LOW_DBM => TxPowerProto::TxPowerLevel(Level::LOW.into()),
TX_POWER_MEDIUM_DBM => TxPowerProto::TxPowerLevel(Level::MEDIUM.into()),
TX_POWER_HIGH_DBM => TxPowerProto::TxPowerLevel(Level::HIGH.into()),
dbm => TxPowerProto::Dbm(dbm.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build() {
let mode = AdvertiseMode::new(Duration::from_millis(200));
let tx_power_level = TxPowerLevel::new(-1);
let timeout = Duration::from_millis(8000);
let settings = AdvertiseSettingsBuilder::new()
.mode(mode)
.tx_power_level(tx_power_level)
.scannable()
.timeout(timeout)
.build();
assert_eq!(
AdvertiseSettings { mode, tx_power_level, scannable: true, timeout: Some(timeout) },
settings
)
}
#[test]
fn test_from_proto_succeeds() {
let interval = IntervalProto::Milliseconds(150);
let tx_power = TxPowerProto::Dbm(3);
let timeout_ms = 5555;
let proto = AdvertiseSettingsProto {
interval: Some(interval.clone()),
tx_power: Some(tx_power.clone()),
scannable: true,
timeout: timeout_ms,
..Default::default()
};
let settings = AdvertiseSettings::from_proto(&proto);
assert!(settings.is_ok());
let tx_power: Result<TxPowerLevel, _> = (&tx_power).try_into();
assert!(tx_power.is_ok());
let tx_power_level = tx_power.unwrap();
let exp_settings = AdvertiseSettingsBuilder::new()
.mode((&interval).into())
.tx_power_level(tx_power_level)
.scannable()
.timeout(Duration::from_millis(timeout_ms))
.build();
assert_eq!(exp_settings, settings.unwrap());
}
#[test]
fn test_from_proto_fails() {
let proto = AdvertiseSettingsProto {
tx_power: Some(TxPowerProto::Dbm((std::i8::MAX as i32) + 1)),
..Default::default()
};
assert!(AdvertiseSettings::from_proto(&proto).is_err());
}
#[test]
fn test_into_proto() {
let proto = AdvertiseSettingsProto {
interval: Some(IntervalProto::Milliseconds(123)),
tx_power: Some(TxPowerProto::Dbm(-3)),
scannable: true,
timeout: 1234,
..Default::default()
};
let settings = AdvertiseSettings::from_proto(&proto);
assert!(settings.is_ok());
let settings: Result<AdvertiseSettingsProto, _> = settings.as_ref().unwrap().try_into();
assert!(settings.is_ok());
assert_eq!(proto, settings.unwrap());
}
#[test]
fn test_from_proto_default() {
let proto = AdvertiseSettingsProto {
tx_power: Default::default(),
interval: Default::default(),
..Default::default()
};
let settings = AdvertiseSettings::from_proto(&proto);
assert!(settings.is_ok());
let settings = settings.unwrap();
let tx_power: i8 = proto
.tx_power
.as_ref()
.map(|proto| TxPowerLevel::try_from(proto).unwrap())
.unwrap_or_default()
.dbm;
let interval: Duration =
proto.interval.as_ref().map(AdvertiseMode::from).unwrap_or_default().interval;
assert_eq!(TX_POWER_LOW_DBM, tx_power);
assert_eq!(Duration::from_millis(MODE_LOW_POWER_MS), interval);
}
#[test]
fn test_from_proto_timeout_unset() {
let proto = AdvertiseSettingsProto::default();
let settings = AdvertiseSettings::from_proto(&proto);
assert!(settings.is_ok());
let settings = settings.unwrap();
assert!(settings.timeout.is_none());
}
}