• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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