const LEGACY_ADV_DATA_CAPACITY: usize = 31;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AddressType {
Public,
RandomStatic,
ResolvablePrivate,
NonResolvablePrivate,
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct RssiDbm(i8);
impl RssiDbm {
pub const fn from_controller(value: i8) -> Option<Self> {
if value == 0x7f {
None
} else {
Some(Self(value))
}
}
pub const fn get(self) -> i8 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DisconnectReason(u32);
impl DisconnectReason {
pub const fn from_vendor(value: u32) -> Self {
Self(value)
}
pub const fn vendor_code(self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BluetoothAddress {
bytes: [u8; 6],
kind: AddressType,
}
impl BluetoothAddress {
pub const fn public(bytes: [u8; 6]) -> Option<Self> {
if all_equal(bytes, 0) {
None
} else {
Some(Self {
bytes,
kind: AddressType::Public,
})
}
}
pub const fn random_static(bytes: [u8; 6]) -> Option<Self> {
let random_part_is_zero = bytes[0] == 0
&& bytes[1] == 0
&& bytes[2] == 0
&& bytes[3] == 0
&& bytes[4] == 0
&& bytes[5] & 0x3f == 0;
let random_part_is_one = bytes[0] == 0xff
&& bytes[1] == 0xff
&& bytes[2] == 0xff
&& bytes[3] == 0xff
&& bytes[4] == 0xff
&& bytes[5] & 0x3f == 0x3f;
if bytes[5] & 0xc0 != 0xc0 || random_part_is_zero || random_part_is_one {
None
} else {
Some(Self {
bytes,
kind: AddressType::RandomStatic,
})
}
}
pub const fn random(bytes: [u8; 6]) -> Option<Self> {
match bytes[5] & 0xc0 {
0xc0 => Self::random_static(bytes),
0x40 => {
let random_part =
bytes[3] as u32 | ((bytes[4] as u32) << 8) | (((bytes[5] & 0x3f) as u32) << 16);
if random_part == 0 || random_part == 0x3f_ffff {
None
} else {
Some(Self {
bytes,
kind: AddressType::ResolvablePrivate,
})
}
}
0x00 => {
if all_equal(bytes, 0) {
None
} else {
Some(Self {
bytes,
kind: AddressType::NonResolvablePrivate,
})
}
}
_ => None,
}
}
pub const fn bytes(self) -> [u8; 6] {
self.bytes
}
pub const fn address_type(self) -> AddressType {
self.kind
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Bonding {
Disabled,
Enabled,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IoCapability {
DisplayOnly,
DisplayYesNo,
KeyboardOnly,
NoInputNoOutput,
KeyboardDisplay,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SecurityRequirement {
Encrypted,
Authenticated,
SecureConnectionsAuthenticated,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SecurityConfig {
bonding: Bonding,
io_capability: IoCapability,
requirement: SecurityRequirement,
}
impl SecurityConfig {
pub const fn new(
bonding: Bonding,
io_capability: IoCapability,
requirement: SecurityRequirement,
) -> Self {
Self {
bonding,
io_capability,
requirement,
}
}
pub const fn bonding(self) -> Bonding {
self.bonding
}
pub const fn io_capability(self) -> IoCapability {
self.io_capability
}
pub const fn requirement(self) -> SecurityRequirement {
self.requirement
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PairingState {
NotPaired,
Pairing,
Paired,
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct Passkey(u32);
impl Passkey {
pub const MAX: u32 = 999_999;
pub const fn try_new(value: u32) -> Option<Self> {
if value <= Self::MAX {
Some(Self(value))
} else {
None
}
}
#[doc(hidden)]
pub const fn as_u32(self) -> u32 {
self.0
}
}
impl core::fmt::Debug for Passkey {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str("Passkey([REDACTED])")
}
}
const fn all_equal(bytes: [u8; 6], value: u8) -> bool {
bytes[0] == value
&& bytes[1] == value
&& bytes[2] == value
&& bytes[3] == value
&& bytes[4] == value
&& bytes[5] == value
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct AdvertisingInterval(u16);
impl AdvertisingInterval {
pub const fn try_from_units(units: u16) -> Option<Self> {
if units < 0x20 || units > 0x4000 {
None
} else {
Some(Self(units))
}
}
pub const fn as_units(self) -> u16 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AdvertisingTiming {
minimum: AdvertisingInterval,
maximum: AdvertisingInterval,
}
impl AdvertisingTiming {
pub const fn try_new(
minimum: AdvertisingInterval,
maximum: AdvertisingInterval,
) -> Option<Self> {
if minimum.0 > maximum.0 {
None
} else {
Some(Self { minimum, maximum })
}
}
pub const fn minimum(self) -> AdvertisingInterval {
self.minimum
}
pub const fn maximum(self) -> AdvertisingInterval {
self.maximum
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AdvertisingChannels(u8);
impl AdvertisingChannels {
pub const ALL: Self = Self(0x07);
pub const fn try_from_bits(bits: u8) -> Option<Self> {
if bits == 0 || bits & !0x07 != 0 {
None
} else {
Some(Self(bits))
}
}
pub const fn bits(self) -> u8 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AdvertisingPayload {
bytes: [u8; LEGACY_ADV_DATA_CAPACITY],
len: u8,
}
impl AdvertisingPayload {
pub fn try_from_slice(value: &[u8]) -> Option<Self> {
if value.len() > LEGACY_ADV_DATA_CAPACITY {
return None;
}
let mut bytes = [0; LEGACY_ADV_DATA_CAPACITY];
bytes[..value.len()].copy_from_slice(value);
Some(Self {
bytes,
len: value.len() as u8,
})
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len as usize]
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AdvertisingConfig {
timing: AdvertisingTiming,
channels: AdvertisingChannels,
payload: AdvertisingPayload,
}
impl AdvertisingConfig {
pub const fn new(
timing: AdvertisingTiming,
channels: AdvertisingChannels,
payload: AdvertisingPayload,
) -> Self {
Self {
timing,
channels,
payload,
}
}
pub const fn timing(self) -> AdvertisingTiming {
self.timing
}
pub const fn channels(self) -> AdvertisingChannels {
self.channels
}
pub const fn payload(&self) -> &AdvertisingPayload {
&self.payload
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ScanInterval(u16);
impl ScanInterval {
pub const fn try_from_units(units: u16) -> Option<Self> {
if units < 0x0004 || units > 0x4000 {
None
} else {
Some(Self(units))
}
}
pub const fn as_units(self) -> u16 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ScanTiming {
interval: ScanInterval,
window: ScanInterval,
}
impl ScanTiming {
pub const fn try_new(interval: ScanInterval, window: ScanInterval) -> Option<Self> {
if window.0 > interval.0 {
None
} else {
Some(Self { interval, window })
}
}
pub const fn interval(self) -> ScanInterval {
self.interval
}
pub const fn window(self) -> ScanInterval {
self.window
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ScanMode {
Passive,
Active,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ScanConfig {
timing: ScanTiming,
mode: ScanMode,
filter_duplicates: bool,
}
impl ScanConfig {
pub const fn new(timing: ScanTiming, mode: ScanMode, filter_duplicates: bool) -> Self {
Self {
timing,
mode,
filter_duplicates,
}
}
pub const fn timing(self) -> ScanTiming {
self.timing
}
pub const fn mode(self) -> ScanMode {
self.mode
}
pub const fn filter_duplicates(self) -> bool {
self.filter_duplicates
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GattUuid {
Uuid16(u16),
Uuid128([u8; 16]),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GattPermissions(u8);
impl GattPermissions {
pub const READ: Self = Self(1 << 0);
pub const WRITE: Self = Self(1 << 1);
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
#[doc(hidden)]
pub const fn __bits(self) -> u8 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GattProperties(u8);
impl GattProperties {
pub const READ: Self = Self(1 << 0);
pub const WRITE: Self = Self(1 << 1);
pub const WRITE_WITHOUT_RESPONSE: Self = Self(1 << 2);
pub const NOTIFY: Self = Self(1 << 3);
pub const INDICATE: Self = Self(1 << 4);
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
#[doc(hidden)]
pub const fn __bits(self) -> u8 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GattDescriptorDefinition {
uuid: GattUuid,
permissions: GattPermissions,
initial_value: &'static [u8],
maximum_len: u16,
}
impl GattDescriptorDefinition {
pub const fn try_new(
uuid: GattUuid,
permissions: GattPermissions,
initial_value: &'static [u8],
maximum_len: u16,
) -> Option<Self> {
if maximum_len == 0 || initial_value.len() > maximum_len as usize {
None
} else {
Some(Self {
uuid,
permissions,
initial_value,
maximum_len,
})
}
}
pub const fn uuid(self) -> GattUuid {
self.uuid
}
pub const fn permissions(self) -> GattPermissions {
self.permissions
}
pub const fn initial_value(self) -> &'static [u8] {
self.initial_value
}
pub const fn maximum_len(self) -> u16 {
self.maximum_len
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GattCharacteristicDefinition {
uuid: GattUuid,
permissions: GattPermissions,
properties: GattProperties,
initial_value: &'static [u8],
maximum_len: u16,
descriptors: &'static [GattDescriptorDefinition],
}
impl GattCharacteristicDefinition {
pub const fn try_new(
uuid: GattUuid,
permissions: GattPermissions,
properties: GattProperties,
initial_value: &'static [u8],
maximum_len: u16,
descriptors: &'static [GattDescriptorDefinition],
) -> Option<Self> {
if maximum_len == 0 || initial_value.len() > maximum_len as usize {
return None;
}
let publishes = properties.contains(GattProperties::NOTIFY)
|| properties.contains(GattProperties::INDICATE);
if publishes && !has_ccc_descriptor(descriptors) {
return None;
}
Some(Self {
uuid,
permissions,
properties,
initial_value,
maximum_len,
descriptors,
})
}
pub const fn uuid(self) -> GattUuid {
self.uuid
}
pub const fn permissions(self) -> GattPermissions {
self.permissions
}
pub const fn properties(self) -> GattProperties {
self.properties
}
pub const fn initial_value(self) -> &'static [u8] {
self.initial_value
}
pub const fn maximum_len(self) -> u16 {
self.maximum_len
}
pub const fn descriptors(self) -> &'static [GattDescriptorDefinition] {
self.descriptors
}
}
const fn has_ccc_descriptor(descriptors: &[GattDescriptorDefinition]) -> bool {
let mut index = 0;
while index < descriptors.len() {
if matches!(descriptors[index].uuid, GattUuid::Uuid16(0x2902)) {
return true;
}
index += 1;
}
false
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GattServiceDefinition {
uuid: GattUuid,
primary: bool,
characteristics: &'static [GattCharacteristicDefinition],
}
impl GattServiceDefinition {
pub const fn try_new(
uuid: GattUuid,
primary: bool,
characteristics: &'static [GattCharacteristicDefinition],
) -> Option<Self> {
if characteristics.is_empty() {
None
} else {
Some(Self {
uuid,
primary,
characteristics,
})
}
}
pub const fn uuid(self) -> GattUuid {
self.uuid
}
pub const fn is_primary(self) -> bool {
self.primary
}
pub const fn characteristics(self) -> &'static [GattCharacteristicDefinition] {
self.characteristics
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GattServerDefinition {
app_uuid: GattUuid,
services: &'static [GattServiceDefinition],
}
impl GattServerDefinition {
pub const fn try_new(
app_uuid: GattUuid,
services: &'static [GattServiceDefinition],
) -> Option<Self> {
if services.is_empty() {
None
} else {
Some(Self { app_uuid, services })
}
}
pub const fn app_uuid(self) -> GattUuid {
self.app_uuid
}
pub const fn services(self) -> &'static [GattServiceDefinition] {
self.services
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_invalid_addresses_and_timing() {
assert!(BluetoothAddress::public([0; 6]).is_none());
assert!(BluetoothAddress::random_static([1, 2, 3, 4, 5, 0x80]).is_none());
assert!(BluetoothAddress::random_static([0, 0, 0, 0, 0, 0xc0]).is_none());
assert!(BluetoothAddress::random_static([0xff; 6]).is_none());
assert!(BluetoothAddress::random([1, 2, 3, 4, 5, 0x80]).is_none());
assert!(BluetoothAddress::random([0; 6]).is_none());
assert!(AdvertisingInterval::try_from_units(0x1f).is_none());
let interval = ScanInterval::try_from_units(0x20).unwrap();
let window = ScanInterval::try_from_units(0x30).unwrap();
assert!(ScanTiming::try_new(interval, window).is_none());
}
#[test]
fn classifies_controller_random_addresses() {
let static_address = BluetoothAddress::random([1, 2, 3, 4, 5, 0xc0]).unwrap();
assert_eq!(static_address.address_type(), AddressType::RandomStatic);
let resolvable = BluetoothAddress::random([1, 2, 3, 4, 5, 0x40]).unwrap();
assert_eq!(resolvable.address_type(), AddressType::ResolvablePrivate);
let non_resolvable = BluetoothAddress::random([1, 2, 3, 4, 5, 0x00]).unwrap();
assert_eq!(
non_resolvable.address_type(),
AddressType::NonResolvablePrivate
);
}
#[test]
fn bounds_legacy_advertising_data() {
assert!(AdvertisingPayload::try_from_slice(&[0; 31]).is_some());
assert!(AdvertisingPayload::try_from_slice(&[0; 32]).is_none());
assert!(AdvertisingChannels::try_from_bits(0).is_none());
assert!(AdvertisingChannels::try_from_bits(0x08).is_none());
}
#[test]
fn security_config_preserves_explicit_policy() {
const CONFIG: SecurityConfig = SecurityConfig::new(
Bonding::Enabled,
IoCapability::DisplayYesNo,
SecurityRequirement::SecureConnectionsAuthenticated,
);
assert_eq!(CONFIG.bonding(), Bonding::Enabled);
assert_eq!(CONFIG.io_capability(), IoCapability::DisplayYesNo);
assert_eq!(
CONFIG.requirement(),
SecurityRequirement::SecureConnectionsAuthenticated
);
assert_eq!(PairingState::NotPaired, PairingState::NotPaired);
}
#[test]
fn passkey_is_bounded_and_redacted() {
assert_eq!(Passkey::try_new(0).unwrap().as_u32(), 0);
assert_eq!(Passkey::try_new(Passkey::MAX).unwrap().as_u32(), 999_999);
assert!(Passkey::try_new(Passkey::MAX + 1).is_none());
assert_eq!(
std::format!("{:?}", Passkey::try_new(123_456).unwrap()),
"Passkey([REDACTED])"
);
}
#[test]
fn validates_static_gatt_database_relations() {
const CCC: GattDescriptorDefinition = GattDescriptorDefinition::try_new(
GattUuid::Uuid16(0x2902),
GattPermissions::READ.union(GattPermissions::WRITE),
&[0, 0],
2,
)
.unwrap();
const CHARACTERISTIC: GattCharacteristicDefinition = GattCharacteristicDefinition::try_new(
GattUuid::Uuid16(0xabcd),
GattPermissions::READ.union(GattPermissions::WRITE),
GattProperties::READ
.union(GattProperties::WRITE)
.union(GattProperties::NOTIFY),
b"U3",
16,
&[CCC],
)
.unwrap();
const SERVICE: GattServiceDefinition =
GattServiceDefinition::try_new(GattUuid::Uuid16(0xcdef), true, &[CHARACTERISTIC])
.unwrap();
const DATABASE: GattServerDefinition =
GattServerDefinition::try_new(GattUuid::Uuid16(0xb301), &[SERVICE]).unwrap();
assert_eq!(DATABASE.services()[0], SERVICE);
assert!(
GattCharacteristicDefinition::try_new(
GattUuid::Uuid16(1),
GattPermissions::READ,
GattProperties::NOTIFY,
&[],
1,
&[],
)
.is_none()
);
assert!(GattServiceDefinition::try_new(GattUuid::Uuid16(1), true, &[]).is_none());
}
}