1 // Copyright 2023 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 use super::packets::link_layer::LegacyAdvertisingType; 16 use netsim_proto::model::chip::ble_beacon::{ 17 advertise_settings::{ 18 AdvertiseMode as Mode, AdvertiseTxPower as Level, Interval as IntervalProto, 19 Tx_power as TxPowerProto, 20 }, 21 AdvertiseSettings as AdvertiseSettingsProto, 22 }; 23 24 use std::time::Duration; 25 26 // From packages/modules/Bluetooth/framework/java/android/bluetooth/le/BluetoothLeAdvertiser.java#151 27 const MODE_LOW_POWER_MS: u64 = 1000; 28 const MODE_BALANCED_MS: u64 = 250; 29 const MODE_LOW_LATENCY_MS: u64 = 100; 30 31 // From packages/modules/Bluetooth/framework/java/android/bluetooth/le/BluetoothLeAdvertiser.java#159 32 const TX_POWER_ULTRA_LOW_DBM: i8 = -21; 33 const TX_POWER_LOW_DBM: i8 = -15; 34 const TX_POWER_MEDIUM_DBM: i8 = -7; 35 const TX_POWER_HIGH_DBM: i8 = 1; 36 37 /// Configurable settings for ble beacon advertisements. 38 #[derive(Debug, PartialEq)] 39 pub struct AdvertiseSettings { 40 /// Time interval between advertisements. 41 pub mode: AdvertiseMode, 42 /// Transmit power level for advertisements and scan responses. 43 pub tx_power_level: TxPowerLevel, 44 /// Whether the beacon will respond to scan requests. 45 pub scannable: bool, 46 /// How long to send advertisements for before stopping. 47 pub timeout: Option<Duration>, 48 } 49 50 impl AdvertiseSettings { 51 /// Returns a new advertise settings builder with no fields. builder() -> AdvertiseSettingsBuilder52 pub fn builder() -> AdvertiseSettingsBuilder { 53 AdvertiseSettingsBuilder::default() 54 } 55 56 /// Returns a new advertise settings with fields from a protobuf. from_proto(proto: &AdvertiseSettingsProto) -> Result<Self, String>57 pub fn from_proto(proto: &AdvertiseSettingsProto) -> Result<Self, String> { 58 proto.try_into() 59 } 60 61 /// Returns the PDU type of advertise packets with the provided settings get_packet_type(&self) -> LegacyAdvertisingType62 pub fn get_packet_type(&self) -> LegacyAdvertisingType { 63 if self.scannable { 64 LegacyAdvertisingType::AdvScanInd 65 } else { 66 LegacyAdvertisingType::AdvNonconnInd 67 } 68 } 69 } 70 71 impl TryFrom<&AdvertiseSettingsProto> for AdvertiseSettings { 72 type Error = String; 73 try_from(value: &AdvertiseSettingsProto) -> Result<Self, Self::Error>74 fn try_from(value: &AdvertiseSettingsProto) -> Result<Self, Self::Error> { 75 let mut builder = AdvertiseSettingsBuilder::default(); 76 77 if let Some(mode) = value.interval.as_ref() { 78 builder.mode(mode.into()); 79 } 80 81 if let Some(tx_power) = value.tx_power.as_ref() { 82 builder.tx_power_level(tx_power.try_into()?); 83 } 84 85 if value.scannable { 86 builder.scannable(); 87 } 88 89 if value.timeout != u64::default() { 90 builder.timeout(Duration::from_millis(value.timeout)); 91 } 92 93 Ok(builder.build()) 94 } 95 } 96 97 impl TryFrom<&AdvertiseSettings> for AdvertiseSettingsProto { 98 type Error = String; 99 try_from(value: &AdvertiseSettings) -> Result<Self, Self::Error>100 fn try_from(value: &AdvertiseSettings) -> Result<Self, Self::Error> { 101 Ok(AdvertiseSettingsProto { 102 interval: Some(value.mode.try_into()?), 103 tx_power: Some(value.tx_power_level.into()), 104 scannable: value.scannable, 105 timeout: value.timeout.unwrap_or_default().as_millis().try_into().map_err(|_| { 106 String::from("could not convert timeout to millis: must fit in a u64") 107 })?, 108 ..Default::default() 109 }) 110 } 111 } 112 113 #[derive(Default)] 114 /// Builder for BLE beacon advertise settings. 115 pub struct AdvertiseSettingsBuilder { 116 mode: Option<AdvertiseMode>, 117 tx_power_level: Option<TxPowerLevel>, 118 scannable: bool, 119 timeout: Option<Duration>, 120 } 121 122 impl AdvertiseSettingsBuilder { 123 /// Returns a new advertise settings builder with empty fields. new() -> Self124 pub fn new() -> Self { 125 Self::default() 126 } 127 128 /// Build the advertise settings. build(&self) -> AdvertiseSettings129 pub fn build(&self) -> AdvertiseSettings { 130 AdvertiseSettings { 131 mode: self.mode.unwrap_or_default(), 132 tx_power_level: self.tx_power_level.unwrap_or_default(), 133 scannable: self.scannable, 134 timeout: self.timeout, 135 } 136 } 137 138 /// Set the advertise mode. mode(&mut self, mode: AdvertiseMode) -> &mut Self139 pub fn mode(&mut self, mode: AdvertiseMode) -> &mut Self { 140 self.mode = Some(mode); 141 self 142 } 143 144 /// Set the transmit power level. tx_power_level(&mut self, tx_power_level: TxPowerLevel) -> &mut Self145 pub fn tx_power_level(&mut self, tx_power_level: TxPowerLevel) -> &mut Self { 146 self.tx_power_level = Some(tx_power_level); 147 self 148 } 149 150 /// Set whether the beacon will respond to scan requests. scannable(&mut self) -> &mut Self151 pub fn scannable(&mut self) -> &mut Self { 152 self.scannable = true; 153 self 154 } 155 156 /// Set how long the beacon will send advertisements for. timeout(&mut self, timeout: Duration) -> &mut Self157 pub fn timeout(&mut self, timeout: Duration) -> &mut Self { 158 self.timeout = Some(timeout); 159 self 160 } 161 } 162 163 /// A BLE beacon advertise mode. Can be casted to/from a protobuf message. 164 #[derive(Debug, Copy, Clone, PartialEq)] 165 pub struct AdvertiseMode { 166 /// The time interval between advertisements. 167 pub interval: Duration, 168 } 169 170 impl AdvertiseMode { 171 /// Create an `AdvertiseMode` from an `std::time::Duration` representing the time interval between advertisements. new(interval: Duration) -> Self172 pub fn new(interval: Duration) -> Self { 173 AdvertiseMode { interval } 174 } 175 } 176 177 impl Default for AdvertiseMode { default() -> Self178 fn default() -> Self { 179 Self { interval: Duration::from_millis(MODE_LOW_POWER_MS) } 180 } 181 } 182 183 impl From<&IntervalProto> for AdvertiseMode { from(value: &IntervalProto) -> Self184 fn from(value: &IntervalProto) -> Self { 185 Self { 186 interval: Duration::from_millis(match value { 187 IntervalProto::Milliseconds(ms) => *ms, 188 IntervalProto::AdvertiseMode(mode) => match mode.enum_value_or_default() { 189 Mode::LOW_POWER => MODE_LOW_POWER_MS, 190 Mode::BALANCED => MODE_BALANCED_MS, 191 Mode::LOW_LATENCY => MODE_LOW_LATENCY_MS, 192 }, 193 _ => MODE_LOW_POWER_MS, 194 }), 195 } 196 } 197 } 198 199 impl TryFrom<AdvertiseMode> for IntervalProto { 200 type Error = String; 201 try_from(value: AdvertiseMode) -> Result<Self, Self::Error>202 fn try_from(value: AdvertiseMode) -> Result<Self, Self::Error> { 203 Ok( 204 match value.interval.as_millis().try_into().map_err(|_| { 205 String::from("failed to convert interval: duration as millis must fit in a u64") 206 })? { 207 MODE_LOW_POWER_MS => IntervalProto::AdvertiseMode(Mode::LOW_POWER.into()), 208 MODE_BALANCED_MS => IntervalProto::AdvertiseMode(Mode::BALANCED.into()), 209 MODE_LOW_LATENCY_MS => IntervalProto::AdvertiseMode(Mode::LOW_LATENCY.into()), 210 ms => IntervalProto::Milliseconds(ms), 211 }, 212 ) 213 } 214 } 215 216 /// A BLE beacon transmit power level. Can be casted to/from a protobuf message. 217 #[derive(Debug, Copy, Clone, PartialEq)] 218 pub struct TxPowerLevel { 219 /// The transmit power in dBm. 220 pub dbm: i8, 221 } 222 223 impl TxPowerLevel { 224 /// Create a `TxPowerLevel` from an `i8` measuring power in dBm. new(dbm: i8) -> Self225 pub fn new(dbm: i8) -> Self { 226 TxPowerLevel { dbm } 227 } 228 } 229 230 impl Default for TxPowerLevel { default() -> Self231 fn default() -> Self { 232 TxPowerLevel { dbm: TX_POWER_LOW_DBM } 233 } 234 } 235 236 impl TryFrom<&TxPowerProto> for TxPowerLevel { 237 type Error = String; 238 try_from(value: &TxPowerProto) -> Result<Self, Self::Error>239 fn try_from(value: &TxPowerProto) -> Result<Self, Self::Error> { 240 Ok(Self { 241 dbm: (match value { 242 TxPowerProto::Dbm(dbm) => (*dbm) 243 .try_into() 244 .map_err(|_| "failed to convert tx power level: it must fit in an i8")?, 245 TxPowerProto::TxPowerLevel(level) => match level.enum_value_or_default() { 246 Level::ULTRA_LOW => TX_POWER_ULTRA_LOW_DBM, 247 Level::LOW => TX_POWER_LOW_DBM, 248 Level::MEDIUM => TX_POWER_MEDIUM_DBM, 249 Level::HIGH => TX_POWER_HIGH_DBM, 250 }, 251 _ => TX_POWER_LOW_DBM, 252 }), 253 }) 254 } 255 } 256 257 impl From<TxPowerLevel> for TxPowerProto { from(value: TxPowerLevel) -> Self258 fn from(value: TxPowerLevel) -> Self { 259 match value.dbm { 260 TX_POWER_ULTRA_LOW_DBM => TxPowerProto::TxPowerLevel(Level::ULTRA_LOW.into()), 261 TX_POWER_LOW_DBM => TxPowerProto::TxPowerLevel(Level::LOW.into()), 262 TX_POWER_MEDIUM_DBM => TxPowerProto::TxPowerLevel(Level::MEDIUM.into()), 263 TX_POWER_HIGH_DBM => TxPowerProto::TxPowerLevel(Level::HIGH.into()), 264 dbm => TxPowerProto::Dbm(dbm.into()), 265 } 266 } 267 } 268 269 #[cfg(test)] 270 mod tests { 271 use super::*; 272 273 #[test] test_build()274 fn test_build() { 275 let mode = AdvertiseMode::new(Duration::from_millis(200)); 276 let tx_power_level = TxPowerLevel::new(-1); 277 let timeout = Duration::from_millis(8000); 278 279 let settings = AdvertiseSettingsBuilder::new() 280 .mode(mode) 281 .tx_power_level(tx_power_level) 282 .scannable() 283 .timeout(timeout) 284 .build(); 285 286 assert_eq!( 287 AdvertiseSettings { mode, tx_power_level, scannable: true, timeout: Some(timeout) }, 288 settings 289 ) 290 } 291 292 #[test] test_from_proto_succeeds()293 fn test_from_proto_succeeds() { 294 let interval = IntervalProto::Milliseconds(150); 295 let tx_power = TxPowerProto::Dbm(3); 296 let timeout_ms = 5555; 297 298 let proto = AdvertiseSettingsProto { 299 interval: Some(interval.clone()), 300 tx_power: Some(tx_power.clone()), 301 scannable: true, 302 timeout: timeout_ms, 303 ..Default::default() 304 }; 305 306 let settings = AdvertiseSettings::from_proto(&proto); 307 assert!(settings.is_ok()); 308 309 let tx_power: Result<TxPowerLevel, _> = (&tx_power).try_into(); 310 assert!(tx_power.is_ok()); 311 let tx_power_level = tx_power.unwrap(); 312 313 let exp_settings = AdvertiseSettingsBuilder::new() 314 .mode((&interval).into()) 315 .tx_power_level(tx_power_level) 316 .scannable() 317 .timeout(Duration::from_millis(timeout_ms)) 318 .build(); 319 320 assert_eq!(exp_settings, settings.unwrap()); 321 } 322 323 #[test] test_from_proto_fails()324 fn test_from_proto_fails() { 325 let proto = AdvertiseSettingsProto { 326 tx_power: Some(TxPowerProto::Dbm((std::i8::MAX as i32) + 1)), 327 ..Default::default() 328 }; 329 330 assert!(AdvertiseSettings::from_proto(&proto).is_err()); 331 } 332 333 #[test] test_into_proto()334 fn test_into_proto() { 335 let proto = AdvertiseSettingsProto { 336 interval: Some(IntervalProto::Milliseconds(123)), 337 tx_power: Some(TxPowerProto::Dbm(-3)), 338 scannable: true, 339 timeout: 1234, 340 ..Default::default() 341 }; 342 343 let settings = AdvertiseSettings::from_proto(&proto); 344 assert!(settings.is_ok()); 345 let settings: Result<AdvertiseSettingsProto, _> = settings.as_ref().unwrap().try_into(); 346 assert!(settings.is_ok()); 347 348 assert_eq!(proto, settings.unwrap()); 349 } 350 351 #[test] test_from_proto_default()352 fn test_from_proto_default() { 353 let proto = AdvertiseSettingsProto { 354 tx_power: Default::default(), 355 interval: Default::default(), 356 ..Default::default() 357 }; 358 359 let settings = AdvertiseSettings::from_proto(&proto); 360 assert!(settings.is_ok()); 361 let settings = settings.unwrap(); 362 363 let tx_power: i8 = proto 364 .tx_power 365 .as_ref() 366 .map(|proto| TxPowerLevel::try_from(proto).unwrap()) 367 .unwrap_or_default() 368 .dbm; 369 let interval: Duration = 370 proto.interval.as_ref().map(AdvertiseMode::from).unwrap_or_default().interval; 371 372 assert_eq!(TX_POWER_LOW_DBM, tx_power); 373 assert_eq!(Duration::from_millis(MODE_LOW_POWER_MS), interval); 374 } 375 376 #[test] test_from_proto_timeout_unset()377 fn test_from_proto_timeout_unset() { 378 let proto = AdvertiseSettingsProto::default(); 379 380 let settings = AdvertiseSettings::from_proto(&proto); 381 assert!(settings.is_ok()); 382 let settings = settings.unwrap(); 383 384 assert!(settings.timeout.is_none()); 385 } 386 } 387