// 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. //! Builder for Advertising Data use netsim_proto::model::{ chip::ble_beacon::AdvertiseData as AdvertiseDataProto, chip_create::BleBeaconCreate as BleBeaconCreateProto, }; use std::convert::TryInto; use super::advertise_settings::TxPowerLevel; // Core Specification (v5.3 Vol 6 Part B §2.3.1.3 and §2.3.1.4) const MAX_ADV_NONCONN_DATA_LEN: usize = 31; // Assigned Numbers Document (§2.3) const AD_TYPE_COMPLETE_NAME: u8 = 0x09; const AD_TYPE_TX_POWER: u8 = 0x0A; const AD_TYPE_MANUFACTURER_DATA: u8 = 0xFF; #[derive(Debug)] pub struct AdvertiseData { /// Whether or not to include the device name in the packet. pub include_device_name: bool, /// Whether or not to include the transmit power in the packet. pub include_tx_power_level: bool, /// Manufacturer-specific data. pub manufacturer_data: Option>, bytes: Vec, } impl AdvertiseData { /// Returns a new advertise data builder with no fields. pub fn builder(device_name: String, tx_power_level: TxPowerLevel) -> AdvertiseDataBuilder { AdvertiseDataBuilder::new(device_name, tx_power_level) } /// Returns a new advertise data with fields from a protobuf. pub fn from_proto( device_name: String, tx_power_level: TxPowerLevel, proto: &AdvertiseDataProto, ) -> Result { let mut builder = AdvertiseDataBuilder::new(device_name, tx_power_level); if proto.include_device_name { builder.include_device_name(); } if proto.include_tx_power_level { builder.include_tx_power_level(); } if !proto.manufacturer_data.is_empty() { builder.manufacturer_data(proto.manufacturer_data.clone()); } builder.build() } /// Gets the raw bytes to be sent in the advertise data field of a BLE advertise packet. pub fn to_bytes(&self) -> Vec { self.bytes.clone() } } impl From<&AdvertiseData> for AdvertiseDataProto { fn from(value: &AdvertiseData) -> Self { AdvertiseDataProto { include_device_name: value.include_device_name, include_tx_power_level: value.include_tx_power_level, manufacturer_data: value.manufacturer_data.clone().unwrap_or_default(), ..Default::default() } } } #[derive(Default)] /// Builder for the advertise data field of a Bluetooth packet. pub struct AdvertiseDataBuilder { device_name: String, tx_power_level: TxPowerLevel, include_device_name: bool, include_tx_power_level: bool, manufacturer_data: Option>, } impl AdvertiseDataBuilder { /// Returns a new advertise data builder with empty fields. pub fn new(device_name: String, tx_power_level: TxPowerLevel) -> Self { AdvertiseDataBuilder { device_name, tx_power_level, ..Self::default() } } /// Build the advertise data. /// /// Returns a vector of bytes holding the serialized advertise data based on the fields added to the builder, or `Err(String)` if the data would be malformed. pub fn build(&self) -> Result { Ok(AdvertiseData { include_device_name: self.include_device_name, include_tx_power_level: self.include_tx_power_level, manufacturer_data: self.manufacturer_data.clone(), bytes: self.serialize()?, }) } /// Add a complete device name field to the advertise data. pub fn include_device_name(&mut self) -> &mut Self { self.include_device_name = true; self } /// Add a transmit power field to the advertise data. pub fn include_tx_power_level(&mut self) -> &mut Self { self.include_tx_power_level = true; self } /// Add a manufacturer data field to the advertise data. pub fn manufacturer_data(&mut self, manufacturer_data: Vec) -> &mut Self { self.manufacturer_data = Some(manufacturer_data); self } fn serialize(&self) -> Result, String> { let mut bytes = Vec::new(); if self.include_device_name { let device_name = self.device_name.as_bytes(); if device_name.len() > MAX_ADV_NONCONN_DATA_LEN - 2 { return Err(format!( "complete name must be less than {} chars", MAX_ADV_NONCONN_DATA_LEN - 2 )); } bytes.extend(vec![ (1 + device_name.len()) .try_into() .map_err(|_| "complete name must be less than 255 chars")?, AD_TYPE_COMPLETE_NAME, ]); bytes.extend_from_slice(device_name); } if self.include_tx_power_level { bytes.extend(vec![2, AD_TYPE_TX_POWER, self.tx_power_level.dbm as u8]); } if let Some(manufacturer_data) = &self.manufacturer_data { if manufacturer_data.len() < 2 { // Supplement to the Core Specification (v10 Part A §1.4.2) return Err("manufacturer data must be at least 2 bytes".to_string()); } if manufacturer_data.len() > MAX_ADV_NONCONN_DATA_LEN - 2 { return Err(format!( "manufacturer data must be less than {} bytes", MAX_ADV_NONCONN_DATA_LEN - 2 )); } bytes.extend(vec![ (1 + manufacturer_data.len()) .try_into() .map_err(|_| "manufacturer data must be less than 255 bytes")?, AD_TYPE_MANUFACTURER_DATA, ]); bytes.extend_from_slice(manufacturer_data); } if bytes.len() > MAX_ADV_NONCONN_DATA_LEN { return Err(format!( "exceeded maximum advertising packet length of {} bytes", MAX_ADV_NONCONN_DATA_LEN )); } Ok(bytes) } } #[cfg(test)] mod tests { use super::*; use netsim_proto::model::chip::ble_beacon::AdvertiseSettings as AdvertiseSettingsProto; use protobuf::MessageField; const HEADER_LEN: usize = 2; #[test] fn test_from_proto_succeeds() { let device_name = String::from("test-device-name"); let tx_power = TxPowerLevel::new(1); let exp_name_len = HEADER_LEN + device_name.len(); let exp_tx_power_len = HEADER_LEN + 1; let ad = AdvertiseData::from_proto( device_name.clone(), tx_power, &AdvertiseDataProto { include_device_name: true, include_tx_power_level: true, ..Default::default() }, ); assert!(ad.is_ok()); let bytes = ad.unwrap().bytes; assert_eq!(exp_name_len + exp_tx_power_len, bytes.len()); assert_eq!( [ vec![(exp_name_len - 1) as u8, AD_TYPE_COMPLETE_NAME], device_name.into_bytes(), vec![(exp_tx_power_len - 1) as u8, AD_TYPE_TX_POWER, tx_power.dbm as u8] ] .concat(), bytes ); } #[test] fn test_from_proto_fails() { let device_name = "a".repeat(MAX_ADV_NONCONN_DATA_LEN - HEADER_LEN + 1); let data = AdvertiseData::from_proto( device_name, TxPowerLevel::new(0), &AdvertiseDataProto { include_device_name: true, ..Default::default() }, ); assert!(data.is_err()); } #[test] fn test_from_proto_sets_proto_field() { let device_name = String::from("test-device-name"); let tx_power = TxPowerLevel::new(1); let ad_proto = AdvertiseDataProto { include_device_name: true, include_tx_power_level: true, ..Default::default() }; let ad = AdvertiseData::from_proto(device_name.clone(), tx_power, &ad_proto); assert!(ad.is_ok()); assert_eq!(ad_proto, (&ad.unwrap()).into()); } #[test] fn test_set_device_name_succeeds() { let device_name = String::from("test-device-name"); let ad = AdvertiseData::builder(device_name.clone(), TxPowerLevel::default()) .include_device_name() .build(); let exp_len = HEADER_LEN + device_name.len(); assert!(ad.is_ok()); let bytes = ad.unwrap().bytes; assert_eq!(exp_len, bytes.len()); assert_eq!( [vec![(exp_len - 1) as u8, AD_TYPE_COMPLETE_NAME], device_name.into_bytes()].concat(), bytes ); } #[test] fn test_set_device_name_fails() { let device_name = "a".repeat(MAX_ADV_NONCONN_DATA_LEN - HEADER_LEN + 1); let data = AdvertiseData::builder(device_name, TxPowerLevel::default()) .include_device_name() .build(); assert!(data.is_err()); } #[test] fn test_set_tx_power_level() { let tx_power = TxPowerLevel::new(-6); let ad = AdvertiseData::builder(String::default(), tx_power).include_tx_power_level().build(); let exp_len = HEADER_LEN + 1; assert!(ad.is_ok()); let bytes = ad.unwrap().bytes; assert_eq!(exp_len, bytes.len()); assert_eq!(vec![(exp_len - 1) as u8, AD_TYPE_TX_POWER, tx_power.dbm as u8], bytes); } #[test] fn test_set_manufacturer_data_succeeds() { let manufacturer_data = String::from("test-manufacturer-data"); let ad = AdvertiseData::builder(String::default(), TxPowerLevel::default()) .manufacturer_data(manufacturer_data.clone().into_bytes()) .build(); let exp_len = HEADER_LEN + manufacturer_data.len(); assert!(ad.is_ok()); let bytes = ad.unwrap().bytes; assert_eq!(exp_len, bytes.len()); assert_eq!( [vec![(exp_len - 1) as u8, AD_TYPE_MANUFACTURER_DATA], manufacturer_data.into_bytes()] .concat(), bytes ); } #[test] fn test_set_manufacturer_data_fails() { let manufacturer_data = "a".repeat(MAX_ADV_NONCONN_DATA_LEN - HEADER_LEN + 1); let data = AdvertiseData::builder(String::default(), TxPowerLevel::default()) .manufacturer_data(manufacturer_data.into_bytes()) .build(); assert!(data.is_err()); } #[test] fn test_set_name_and_power_succeeds() { let exp_data = [ 0x0F, 0x09, b'g', b'D', b'e', b'v', b'i', b'c', b'e', b'-', b'b', b'e', b'a', b'c', b'o', b'n', 0x02, 0x0A, 0x0, ]; let data = AdvertiseData::builder(String::from("gDevice-beacon"), TxPowerLevel::new(0)) .include_device_name() .include_tx_power_level() .build(); assert!(data.is_ok()); assert_eq!(exp_data, data.unwrap().bytes.as_slice()); } }