blob: 73a3eb8d906ac7611d22eeec2b6cb80a7f9451c7 [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.
//! 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<Vec<u8>>,
bytes: Vec<u8>,
}
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<Self, String> {
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<u8> {
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<Vec<u8>>,
}
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<AdvertiseData, String> {
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<u8>) -> &mut Self {
self.manufacturer_data = Some(manufacturer_data);
self
}
fn serialize(&self) -> Result<Vec<u8>, 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());
}
}