// 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 netsim_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, } 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 { 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 { 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 { 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, tx_power_level: Option, scannable: bool, timeout: Option, } 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 for IntervalProto { type Error = String; fn try_from(value: AdvertiseMode) -> Result { 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 { 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 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 = (&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((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 = 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()); } }